pcli/
config.rs

1use std::path::Path;
2
3use anyhow::{Context, Result};
4#[cfg(feature = "ledger")]
5use penumbra_sdk_custody_ledger_usb::Config as LedgerConfig;
6use penumbra_sdk_stake::GovernanceKey;
7use serde::{Deserialize, Serialize};
8use serde_with::{serde_as, DisplayFromStr};
9use url::Url;
10
11use penumbra_sdk_custody::{
12    encrypted::Config as EncryptedConfig, soft_kms::Config as SoftKmsConfig,
13    threshold::Config as ThresholdConfig,
14};
15use penumbra_sdk_keys::FullViewingKey;
16
17/// Configuration data for `pcli`.
18#[serde_as]
19#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
20pub struct PcliConfig {
21    /// The URL of the gRPC endpoint used to talk to pd.
22    pub grpc_url: Url,
23    /// If set, use a remote view service instead of local synchronization.
24    pub view_url: Option<Url>,
25    /// Disable the scary "you will lose all your money" warning.
26    #[serde(default, skip_serializing_if = "is_default")]
27    pub disable_warning: bool,
28    /// The FVK used for viewing chain data.
29    #[serde_as(as = "DisplayFromStr")]
30    pub full_viewing_key: FullViewingKey,
31    /// The custody backend to use.
32    pub custody: CustodyConfig,
33    /// The governance custody backend to use.
34    pub governance_custody: Option<GovernanceCustodyConfig>,
35}
36
37impl PcliConfig {
38    pub fn load<P: AsRef<Path> + std::fmt::Display>(path: P) -> Result<Self> {
39        let contents = std::fs::read_to_string(&path).context(format!(
40            "pcli config file not found: {}. hint: run 'pcli init' to create new keys",
41            &path
42        ))?;
43        Ok(toml::from_str(&contents)?)
44    }
45
46    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
47        let contents = toml::to_string_pretty(&self)?;
48        std::fs::write(path, contents)?;
49        Ok(())
50    }
51
52    pub fn governance_key(&self) -> GovernanceKey {
53        let fvk = match &self.governance_custody {
54            Some(GovernanceCustodyConfig::SoftKms(SoftKmsConfig { spend_key, .. })) => {
55                spend_key.full_viewing_key()
56            }
57            Some(GovernanceCustodyConfig::Threshold(threshold_config)) => threshold_config.fvk(),
58            Some(GovernanceCustodyConfig::Encrypted { fvk, .. }) => fvk,
59            None => &self.full_viewing_key,
60        };
61        GovernanceKey(fvk.spend_verification_key().clone())
62    }
63}
64
65/// The custody backend to use.
66#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
67#[serde(tag = "backend")]
68#[allow(clippy::large_enum_variant)]
69pub enum CustodyConfig {
70    /// A view-only client that can't sign transactions.
71    ViewOnly,
72    /// A software key management service.
73    SoftKms(SoftKmsConfig),
74    /// A manual threshold custody service.
75    Threshold(ThresholdConfig),
76    /// An encrypted custody service.
77    Encrypted(EncryptedConfig),
78    /// A custody service using an external ledger device.
79    #[cfg(feature = "ledger")]
80    Ledger(LedgerConfig),
81}
82
83/// The governance custody backend to use.
84#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
85#[serde(tag = "backend")]
86#[allow(clippy::large_enum_variant)]
87pub enum GovernanceCustodyConfig {
88    /// A software key management service.
89    SoftKms(SoftKmsConfig),
90    /// A manual threshold custody service.
91    Threshold(ThresholdConfig),
92    /// An encrypted custody service.
93    Encrypted {
94        fvk: FullViewingKey,
95        config: EncryptedConfig,
96    },
97}
98
99impl Default for CustodyConfig {
100    fn default() -> Self {
101        Self::ViewOnly
102    }
103}
104
105/// Helper function for Serde serialization, allowing us to skip serialization
106/// of default config values.  Rationale: if we don't skip serialization of
107/// defaults, if someone serializes a config with some default values, they're
108/// "pinning" the current defaults as their choices for all time, and we have no
109/// way to distinguish between fields they configured explicitly and ones they
110/// passed through from the defaults. If we skip serializing default values,
111/// then we know every value in the config was explicitly set.
112fn is_default<T: Default + Eq>(value: &T) -> bool {
113    *value == T::default()
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn toml_config() {
122        let config = PcliConfig {
123            grpc_url: Url::parse("https://grpc.testnet.penumbra.zone").unwrap(),
124            disable_warning: false,
125            view_url: None,
126            full_viewing_key: penumbra_sdk_keys::test_keys::FULL_VIEWING_KEY.clone(),
127            custody: CustodyConfig::SoftKms(SoftKmsConfig::from(
128                penumbra_sdk_keys::test_keys::SPEND_KEY.clone(),
129            )),
130            governance_custody: None,
131        };
132
133        let mut config2 = config.clone();
134        config2.custody = CustodyConfig::ViewOnly;
135        config2.disable_warning = true;
136
137        let toml_config = toml::to_string_pretty(&config).unwrap();
138        let toml_config2 = toml::to_string_pretty(&config2).unwrap();
139
140        println!("{}", toml_config);
141        println!("{}", toml_config2);
142    }
143}