pcli/command/
migrate.rs

1use crate::App;
2use anyhow::{anyhow, Context, Result};
3use penumbra_sdk_asset::{asset, Value, STAKING_TOKEN_ASSET_ID};
4use penumbra_sdk_keys::FullViewingKey;
5use penumbra_sdk_num::Amount;
6use penumbra_sdk_proto::view::v1::GasPricesRequest;
7use penumbra_sdk_view::ViewClient;
8use penumbra_sdk_wallet::plan::Planner;
9use rand_core::OsRng;
10use std::{collections::HashMap, io::Write};
11use termion::input::TermRead;
12
13fn read_fvk() -> Result<FullViewingKey> {
14    print!("Enter FVK: ");
15    std::io::stdout().flush()?;
16    let fvk_string: String = std::io::stdin().lock().read_line()?.unwrap_or_default();
17
18    fvk_string
19        .parse::<FullViewingKey>()
20        .map_err(|_| anyhow::anyhow!("The provided string is not a valid FullViewingKey."))
21}
22
23#[derive(Debug, clap::Parser)]
24pub enum MigrateCmd {
25    /// Migrate your entire balance to another wallet.
26    ///
27    /// All assets from all accounts in the source wallet will be sent to the destination wallet.
28    /// A FullViewingKey must be provided for the destination wallet.
29    /// All funds will be deposited in the account 0 of the destination wallet,
30    /// minus any gas prices for the migration transaction.
31    #[clap(name = "balance")]
32    Balance,
33}
34
35impl MigrateCmd {
36    #[tracing::instrument(skip(self, app))]
37    pub async fn exec(&self, app: &mut App) -> Result<()> {
38        let gas_prices = app
39            .view
40            .as_mut()
41            .context("view service must be initialized")?
42            .gas_prices(GasPricesRequest {})
43            .await?
44            .into_inner()
45            .gas_prices
46            .expect("gas prices must be available")
47            .try_into()?;
48
49        match self {
50            MigrateCmd::Balance => {
51                let source_fvk = app.config.full_viewing_key.clone();
52
53                let dest_fvk = read_fvk()?;
54
55                let mut planner = Planner::new(OsRng);
56
57                planner
58                    .set_gas_prices(gas_prices)
59                    .set_fee_tier(Default::default());
60
61                // Return all unspent notes from the view service
62                let notes = app
63                    .view
64                    .as_mut()
65                    .context("view service must be initialized")?
66                    .unspent_notes_by_account_and_asset()
67                    .await?;
68
69                let mut account_values: HashMap<(u32, asset::Id), Amount> = HashMap::new();
70
71                for (account, notes) in notes {
72                    for notes in notes.into_values() {
73                        for note in notes {
74                            let position = note.position;
75                            let note = note.note;
76                            let value = note.value();
77                            planner.spend(note, position);
78                            *account_values.entry((account, value.asset_id)).or_default() +=
79                                value.amount;
80                        }
81                    }
82                }
83
84                // We'll use the account with the most amount of the fee token to pay fees.
85                //
86                // If this fails, then it won't be possible to migrate.
87                let (&(largest_account, _), _) = account_values
88                    .iter()
89                    .filter(|((_, asset), _)| *asset == *STAKING_TOKEN_ASSET_ID)
90                    .max_by_key(|&(_, &amount)| amount)
91                    .ok_or(anyhow!("no account with the ability to pay fees exists"))?;
92
93                // Set this account to be the change address.
94                planner.change_address(dest_fvk.payment_address(largest_account.into()).0);
95
96                // Create explicit outputs for the other addresses.
97                for (&(account, asset_id), &amount) in &account_values {
98                    if account == largest_account {
99                        continue;
100                    }
101                    let (address, _) = dest_fvk.payment_address(account.into());
102                    planner.output(Value { asset_id, amount }, address);
103                }
104
105                let memo = format!("Migrating balance from {} to {}", source_fvk, dest_fvk);
106                let plan = planner
107                    .memo(memo)
108                    .plan(
109                        app.view
110                            .as_mut()
111                            .context("view service must be initialized")?,
112                        Default::default(),
113                    )
114                    .await
115                    .context("can't build send transaction")?;
116
117                if plan.actions.is_empty() {
118                    anyhow::bail!("migration plan contained zero actions: is the source wallet already empty?");
119                }
120                app.build_and_submit_transaction(plan).await?;
121
122                Result::Ok(())
123            }
124        }
125    }
126}