pcli/
config.rs

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