Skip to content

Commit

Permalink
pcli: add balance migration command
Browse files Browse the repository at this point in the history
  • Loading branch information
aubrika committed Aug 28, 2024
1 parent 96af7eb commit f450513
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 7 deletions.
18 changes: 12 additions & 6 deletions crates/bin/pcli/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub use debug::DebugCmd;
pub use init::InitCmd;
pub use migrate::MigrateCmd;
pub use query::QueryCmd;
pub use threshold::ThresholdCmd;
pub use tx::TxCmd;
Expand All @@ -11,6 +12,7 @@ use self::{ceremony::CeremonyCmd, tx::TxCmdWithOptions};
mod ceremony;
mod debug;
mod init;
mod migrate;
mod query;
mod threshold;
mod tx;
Expand Down Expand Up @@ -53,18 +55,21 @@ pub enum Command {
/// Create and broadcast a transaction.
#[clap(display_order = 400, visible_alias = "tx")]
Transaction(TxCmdWithOptions),
/// Follow the threshold signing protocol.
#[clap(subcommand, display_order = 500)]
Threshold(ThresholdCmd),
/// Migrate your balance to another wallet.
#[clap(subcommand, display_order = 600)]
Migrate(MigrateCmd),
/// Manage a validator.
#[clap(subcommand, display_order = 900)]
Validator(ValidatorCmd),
/// Display information related to diagnosing problems running Penumbra
#[clap(subcommand, display_order = 999)]
Debug(DebugCmd),
/// Contribute to the summoning ceremony.
#[clap(subcommand, display_order = 990)]
Ceremony(CeremonyCmd),
/// Follow the threshold signing protocol.
#[clap(subcommand, display_order = 500)]
Threshold(ThresholdCmd),
/// Display information related to diagnosing problems running Penumbra
#[clap(subcommand, display_order = 999)]
Debug(DebugCmd),
}

impl Command {
Expand All @@ -79,6 +84,7 @@ impl Command {
Command::Debug(cmd) => cmd.offline(),
Command::Ceremony(_) => false,
Command::Threshold(cmd) => cmd.offline(),
Command::Migrate(_) => false,
}
}
}
129 changes: 129 additions & 0 deletions crates/bin/pcli/src/command/migrate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use std::{
collections::BTreeMap,
fs::{self, File},
io::{Read, Write},
path::PathBuf,
str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};

use anyhow::{ensure, Context, Result};
use decaf377::{Fq, Fr};

use penumbra_asset::{asset, asset::Metadata, Value, STAKING_TOKEN_ASSET_ID};
use penumbra_dex::{lp::position, swap_claim::SwapClaimPlan};
use penumbra_fee::FeeTier;
use penumbra_fee::GasPrices;
use penumbra_keys::{keys::AddressIndex, Address, FullViewingKey};
use penumbra_num::Amount;
use penumbra_proto::view::v1::GasPricesRequest;
use penumbra_shielded_pool::Ics20Withdrawal;
use penumbra_transaction::{gas::swap_claim_gas_cost, Transaction};
use penumbra_view::{SpendableNoteRecord, ViewClient};
use penumbra_wallet::plan::{self, Planner};

use crate::App;
use clap::Parser;
use rand_core::OsRng;

#[derive(Debug, clap::Parser)]
pub enum MigrateCmd {
/// Migrate your entire balance to the wallet of the provided FullViewingKey
#[clap(name = "balance")]
Balance {
/// The FullViewingKey associated with the destination wallet.
#[clap(long)]
to: String,
},
}

impl MigrateCmd {
#[tracing::instrument(skip(self, app))]
pub async fn exec(&self, app: &mut App) -> Result<()> {
let gas_prices: GasPrices = 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 {
MigrateCmd::Balance { to } => {
let source_fvk = app.config.full_viewing_key.clone();

let dest_fvk = to.parse::<FullViewingKey>().map_err(|_| {
anyhow::anyhow!("The provided string is not a valid FullViewingKey.")
})?;

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

let mut memo = format!("Migrating balance from {} to {}", source_fvk, dest_fvk);

let (dest_address, _) = FullViewingKey::payment_address(
&FullViewingKey::from_str(&to[..])?,
AddressIndex::new(0),
);

planner
.set_gas_prices(gas_prices)
.set_fee_tier(FeeTier::default());

// Return all unspent notes from the view service
let notes = app
.view
.as_mut()
.context("view service must be initialized")?
.unspent_notes_by_account_and_asset()
.await?;

// Get all remaining note values after filtering out notes already spent by the planner
let note_values = notes.iter().flat_map(|(_, notes_by_asset)| {
notes_by_asset.iter().flat_map(|(asset, notes)| {
notes.iter().map(move |record| {
let value = asset.value(record.note.amount());

value
})
})
});

// Add all note values to the planner
note_values.clone().for_each(|value| {
planner.output(value, dest_address.clone());
});

let fee = planner
.compute_fee_estimate(&gas_prices, &FeeTier::default())
.expect("there should be a computable fee");

println!("Estimated fee for sending total balance: {:?}", fee);

// defaulting to index 0 (the default account) here means that this operation
// will fail if the default account has insufficient funds for fees
// a more general solution could be finding the first/lowest-indexed account with sufficient funds
// and spending fees from that account
let address_index = AddressIndex::new(0);

let plan = planner
.memo(memo)
.plan(
app.view
.as_mut()
.context("view service must be initialized")?,
address_index,
)
.await
.context("can't build send transaction")?;

app.build_and_submit_transaction(plan).await?;

Result::Ok(())
}
_ => Result::Ok(()),
}
}
}
1 change: 1 addition & 0 deletions crates/bin/pcli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ async fn main() -> Result<()> {
Command::Query(cmd) => cmd.exec(&mut app).await?,
Command::Ceremony(cmd) => cmd.exec(&mut app).await?,
Command::Threshold(cmd) => cmd.exec(&mut app).await?,
Command::Migrate(cmd) => cmd.exec(&mut app).await?,
}

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/core/transaction/src/action_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ impl ActionList {
/// While the gas cost can be computed exactly, the base fee can only be
/// estimated, because the actual base fee paid by the transaction will
/// depend on the gas prices at the time it's accepted on-chain.
fn compute_fee_estimate(&self, gas_prices: &GasPrices, fee_tier: &FeeTier) -> Fee {
pub fn compute_fee_estimate(&self, gas_prices: &GasPrices, fee_tier: &FeeTier) -> Fee {
let base_fee = gas_prices.fee(&self.gas_cost());
base_fee.apply_tier(*fee_tier)
}
Expand Down
10 changes: 10 additions & 0 deletions crates/view/src/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ impl<R: RngCore + CryptoRng> Planner<R> {
}
}

/// Compute a fee estimate for the transaction.

pub fn compute_fee_estimate(
&mut self,
gas_prices: &GasPrices,
fee_tier: &FeeTier,
) -> Result<Fee> {
Ok(self.action_list.compute_fee_estimate(gas_prices, fee_tier))
}

/// Add an arbitrary action to the planner.
pub fn action<A: Into<ActionPlan>>(&mut self, action: A) -> &mut Self {
self.action_list.push(action);
Expand Down

0 comments on commit f450513

Please sign in to comment.