From 492ec643df61dd33dcc76455129d84e94a7a5ee1 Mon Sep 17 00:00:00 2001 From: aubrey Date: Wed, 28 Aug 2024 12:47:34 -0700 Subject: [PATCH] feat(pcli): add balance migration command Adds a new subcommand to pcli for moving the entirety of a wallet's assets to a new wallet, as defined by an FVK. Stores both FVKs in the memo of the transaction. Leaves zero notes in the source wallet, and the dest wallet gets everything, minus whatever gas price was paid. The full-send functionality is achieved by using the change address of the planner, setting it to the dest FVK's 0 address. This approach avoids the issue of dealing with fees outside the planner. It does this by eschewing trying to figure out how much to output, and instead focusing on what to spend (everything). Then, the change address is set to the desired destination, allowing the planner to automatically move the funds as needed, minus fees. Ensures that the FVK is read from stdin, so that it's never placed on the command line. --- crates/bin/pcli/src/command.rs | 18 +++-- crates/bin/pcli/src/command/migrate.rs | 98 ++++++++++++++++++++++++++ crates/bin/pcli/src/main.rs | 1 + 3 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 crates/bin/pcli/src/command/migrate.rs diff --git a/crates/bin/pcli/src/command.rs b/crates/bin/pcli/src/command.rs index c7c787eaf6..420964e6e6 100644 --- a/crates/bin/pcli/src/command.rs +++ b/crates/bin/pcli/src/command.rs @@ -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; @@ -11,6 +12,7 @@ use self::{ceremony::CeremonyCmd, tx::TxCmdWithOptions}; mod ceremony; mod debug; mod init; +mod migrate; mod query; mod threshold; mod tx; @@ -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 { @@ -79,6 +84,7 @@ impl Command { Command::Debug(cmd) => cmd.offline(), Command::Ceremony(_) => false, Command::Threshold(cmd) => cmd.offline(), + Command::Migrate(_) => false, } } } diff --git a/crates/bin/pcli/src/command/migrate.rs b/crates/bin/pcli/src/command/migrate.rs new file mode 100644 index 0000000000..df81b13bed --- /dev/null +++ b/crates/bin/pcli/src/command/migrate.rs @@ -0,0 +1,98 @@ +use crate::App; +use anyhow::{Context, Result}; +use penumbra_keys::FullViewingKey; +use penumbra_proto::view::v1::GasPricesRequest; +use penumbra_view::ViewClient; +use penumbra_wallet::plan::Planner; +use rand_core::OsRng; +use std::{io::Write, str::FromStr}; +use termion::input::TermRead; + +#[derive(Debug, clap::Parser)] +pub enum MigrateCmd { + /// Migrate your entire balance to another wallet. + /// + /// All assets from all accounts in the source wallet will be sent to the destination wallet. + /// A FullViewingKey must be provided for the destination wallet. + /// All funds will be deposited in the account 0 of the destination wallet, + /// minus any gas prices for the migration transaction. + #[clap(name = "balance")] + Balance, +} + +impl MigrateCmd { + #[tracing::instrument(skip(self, app))] + pub async fn exec(&self, app: &mut App) -> 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()?; + + print!("Enter FVK: "); + std::io::stdout().flush()?; + let to: String = std::io::stdin().lock().read_line()?.unwrap_or_default(); + + match self { + MigrateCmd::Balance => { + let source_fvk = app.config.full_viewing_key.clone(); + + let dest_fvk = to.parse::().map_err(|_| { + anyhow::anyhow!("The provided string is not a valid FullViewingKey.") + })?; + + let mut planner = Planner::new(OsRng); + + let (dest_address, _) = FullViewingKey::payment_address( + &FullViewingKey::from_str(&to[..])?, + Default::default(), + ); + + planner + .set_gas_prices(gas_prices) + .set_fee_tier(Default::default()) + .change_address(dest_address); + + // 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?; + + for notes in notes.into_values() { + for notes in notes.into_values() { + for note in notes { + planner.spend(note.note, note.position); + } + } + } + + let memo = format!("Migrating balance from {} to {}", source_fvk, dest_fvk); + let plan = planner + .memo(memo) + .plan( + app.view + .as_mut() + .context("view service must be initialized")?, + Default::default(), + ) + .await + .context("can't build send transaction")?; + + if plan.actions.is_empty() { + anyhow::bail!("migration plan contained zero actions: is the source wallet already empty?"); + } + app.build_and_submit_transaction(plan).await?; + + Result::Ok(()) + } + } + } +} diff --git a/crates/bin/pcli/src/main.rs b/crates/bin/pcli/src/main.rs index b308769aac..29f23a61b9 100644 --- a/crates/bin/pcli/src/main.rs +++ b/crates/bin/pcli/src/main.rs @@ -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(())