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 29, 2024
1 parent 7601c8c commit 8a219f0
Show file tree
Hide file tree
Showing 5 changed files with 185 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,
}
}
}
148 changes: 148 additions & 0 deletions crates/bin/pcli/src/command/migrate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use crate::App;
use anyhow::{Context, Result};
use penumbra_fee::FeeTier;
use penumbra_fee::GasPrices;
use penumbra_keys::{keys::AddressIndex, FullViewingKey};
use penumbra_proto::view::v1::GasPricesRequest;
use penumbra_view::ViewClient;
use penumbra_wallet::plan::Planner;
use rand_core::OsRng;
use std::str::FromStr;

#[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 memo = format!("Migrating balance from {} to {}", source_fvk, dest_fvk);

let (source_address, _) =
FullViewingKey::payment_address(&source_fvk, AddressIndex::new(0));

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().map(|(asset, notes)| {
let sum: u128 = notes
.iter()
.map(|record| u128::from(record.note.amount()))
.sum();

asset.value(sum.into())
})
});

// 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(), &source_address);

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

println!(
"Estimated fee for sending total balance: {:?}",
fee.0.format(&asset_cache)
);

// Update relevant note values to be less the estimated fee

let mut note_values_2 = Vec::new();

note_values.for_each(|value| {
if value.asset_id == fee.0.asset_id {
note_values_2.push(penumbra_asset::Value {
asset_id: value.asset_id,
amount: value.amount - fee.0.amount,
});
} else {
note_values_2.push(penumbra_asset::Value {
asset_id: value.asset_id,
amount: value.amount,
});
}
});

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

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

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

// 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_2
.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(())
}
}
}
}
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
6 changes: 5 additions & 1 deletion crates/core/transaction/src/action_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ pub struct ActionList {
}

impl ActionList {
/// Returns the transaction fee as currently estimated.
pub fn fee(&self) -> Fee {
self.fee
}
/// Returns true if the resulting transaction would require a memo.
pub fn requires_memo(&self) -> bool {
let has_change_outputs = !self.change_outputs.is_empty();
Expand Down Expand Up @@ -124,7 +128,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
19 changes: 19 additions & 0 deletions crates/view/src/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,25 @@ 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,
change_address: &Address,
) -> Fee {

self.action_list.refresh_fee_and_change(
&mut self.rng,
gas_prices,
fee_tier,
change_address,
);

self.action_list.fee()
}

/// 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 8a219f0

Please sign in to comment.