pcli/command/tx/
lqt_vote.rs

1use anyhow::anyhow;
2use penumbra_sdk_asset::asset::REGISTRY;
3use penumbra_sdk_fee::{FeeTier, GasPrices};
4use penumbra_sdk_keys::Address;
5use penumbra_sdk_proto::core::component::sct::v1::{
6    query_service_client::QueryServiceClient as SctQueryServiceClient, EpochByHeightRequest,
7};
8use penumbra_sdk_sct::epoch::Epoch;
9use penumbra_sdk_view::{Planner, ViewClient};
10use rand_core::OsRng;
11
12use crate::App;
13
14async fn fetch_epoch(app: &mut App) -> anyhow::Result<Epoch> {
15    let mut sct_client = SctQueryServiceClient::new(app.pd_channel().await?);
16    let latest_sync_height = app.view().status().await?.full_sync_height;
17    let epoch = sct_client
18        .epoch_by_height(EpochByHeightRequest {
19            height: latest_sync_height,
20        })
21        .await?
22        .into_inner()
23        .epoch
24        .expect("epoch must be available")
25        .into();
26    Ok(epoch)
27}
28
29/// Vote in the current round of the liquidity tournament.
30///
31/// This will plan a transaction which directs all available voting power to a single asset.
32#[derive(Debug, clap::Parser)]
33pub struct LqtVoteCmd {
34    /// The denom string for the asset being voted for.
35    vote: String,
36    /// If provided, make the rewards recipient a particular address instead.
37    ///
38    /// This can also be an integer, indicating an ephemeral address of a sub-account.
39    #[clap(short, long)]
40    rewards_recipient: Option<String>,
41    /// The selected fee tier.
42    #[clap(short, long, default_value_t)]
43    fee_tier: FeeTier,
44}
45
46impl LqtVoteCmd {
47    pub fn offline(&self) -> bool {
48        false
49    }
50
51    fn rewards_addr(&self, app: &App) -> anyhow::Result<Address> {
52        let to_parse = match &self.rewards_recipient {
53            None => {
54                return Ok(app
55                    .config
56                    .full_viewing_key
57                    .ephemeral_address(OsRng, Default::default())
58                    .0)
59            }
60            Some(x) => x,
61        };
62        let maybe_index: Option<u32> = to_parse.parse().ok();
63        if let Some(i) = maybe_index {
64            return Ok(app
65                .config
66                .full_viewing_key
67                .ephemeral_address(OsRng, i.into())
68                .0);
69        }
70        to_parse
71            .parse()
72            .map_err(|_| anyhow!("failed to parse address '{}'", to_parse))
73    }
74
75    pub async fn exec(&self, app: &mut App, gas_prices: GasPrices) -> anyhow::Result<()> {
76        let vote_meta = REGISTRY
77            .parse_denom(&self.vote)
78            .ok_or_else(|| anyhow!("failed to parse denom: '{}'", &self.vote))?;
79        let vote_denom = vote_meta.base_denom();
80
81        let epoch = fetch_epoch(app).await?;
82        let voting_notes = app.view().lqt_voting_notes(epoch.index, None).await?;
83
84        let mut planner = Planner::new(OsRng);
85
86        planner
87            .set_gas_prices(gas_prices)
88            .set_fee_tier(self.fee_tier);
89
90        // First, tell the planner to make all the necessary votes.
91        planner.lqt_vote(
92            u16::try_from(epoch.index)?,
93            vote_denom,
94            self.rewards_addr(app)?,
95            &voting_notes,
96        );
97        // We also want to go ahead and do the consolidation thing,
98        // to reduce the number of votes we need in the next epoch.
99        // To do so, we need to spend all of these notes, and produce one output per
100        // delegator token.
101        for note in voting_notes {
102            planner.spend(note.note, note.position);
103        }
104        // By setting the change address, all of the excess balance we've created
105        // from spending the notes will be directed back to our account.
106        let change_addr = app
107            .config
108            .full_viewing_key
109            .ephemeral_address(OsRng, Default::default())
110            .0;
111        planner.change_address(change_addr.clone());
112
113        let plan = planner.plan(app.view(), Default::default()).await?;
114        app.build_and_submit_transaction(plan).await?;
115
116        Ok(())
117    }
118}