1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use std::path::Path;

use anyhow::{Context, Result};
use penumbra_stake::GovernanceKey;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use url::Url;

use penumbra_custody::{
    encrypted::Config as EncryptedConfig, soft_kms::Config as SoftKmsConfig,
    threshold::Config as ThresholdConfig,
};
use penumbra_keys::FullViewingKey;

/// Configuration data for `pcli`.
#[serde_as]
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct PcliConfig {
    /// The URL of the gRPC endpoint used to talk to pd.
    pub grpc_url: Url,
    /// If set, use a remote view service instead of local synchronization.
    pub view_url: Option<Url>,
    /// Disable the scary "you will lose all your money" warning.
    #[serde(default, skip_serializing_if = "is_default")]
    pub disable_warning: bool,
    /// The FVK used for viewing chain data.
    #[serde_as(as = "DisplayFromStr")]
    pub full_viewing_key: FullViewingKey,
    /// The custody backend to use.
    pub custody: CustodyConfig,
    /// The governance custody backend to use.
    pub governance_custody: Option<GovernanceCustodyConfig>,
}

impl PcliConfig {
    pub fn load<P: AsRef<Path> + std::fmt::Display>(path: P) -> Result<Self> {
        let contents = std::fs::read_to_string(&path).context(format!(
            "pcli config file not found: {}. hint: run 'pcli init' to create new keys",
            &path
        ))?;
        Ok(toml::from_str(&contents)?)
    }

    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
        let contents = toml::to_string_pretty(&self)?;
        std::fs::write(path, contents)?;
        Ok(())
    }

    pub fn governance_key(&self) -> GovernanceKey {
        let fvk = match &self.governance_custody {
            Some(GovernanceCustodyConfig::SoftKms(SoftKmsConfig { spend_key, .. })) => {
                spend_key.full_viewing_key()
            }
            Some(GovernanceCustodyConfig::Threshold(threshold_config)) => threshold_config.fvk(),
            Some(GovernanceCustodyConfig::Encrypted { fvk, .. }) => fvk,
            None => &self.full_viewing_key,
        };
        GovernanceKey(fvk.spend_verification_key().clone())
    }
}

/// The custody backend to use.
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(tag = "backend")]
#[allow(clippy::large_enum_variant)]
pub enum CustodyConfig {
    /// A view-only client that can't sign transactions.
    ViewOnly,
    /// A software key management service.
    SoftKms(SoftKmsConfig),
    /// A manual threshold custody service.
    Threshold(ThresholdConfig),
    /// An encrypted custody service.
    Encrypted(EncryptedConfig),
}

/// The governance custody backend to use.
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(tag = "backend")]
#[allow(clippy::large_enum_variant)]
pub enum GovernanceCustodyConfig {
    /// A software key management service.
    SoftKms(SoftKmsConfig),
    /// A manual threshold custody service.
    Threshold(ThresholdConfig),
    /// An encrypted custody service.
    Encrypted {
        fvk: FullViewingKey,
        config: EncryptedConfig,
    },
}

impl Default for CustodyConfig {
    fn default() -> Self {
        Self::ViewOnly
    }
}

/// Helper function for Serde serialization, allowing us to skip serialization
/// of default config values.  Rationale: if we don't skip serialization of
/// defaults, if someone serializes a config with some default values, they're
/// "pinning" the current defaults as their choices for all time, and we have no
/// way to distinguish between fields they configured explicitly and ones they
/// passed through from the defaults. If we skip serializing default values,
/// then we know every value in the config was explicitly set.
fn is_default<T: Default + Eq>(value: &T) -> bool {
    *value == T::default()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn toml_config() {
        let config = PcliConfig {
            grpc_url: Url::parse("https://grpc.testnet.penumbra.zone").unwrap(),
            disable_warning: false,
            view_url: None,
            full_viewing_key: penumbra_keys::test_keys::FULL_VIEWING_KEY.clone(),
            custody: CustodyConfig::SoftKms(SoftKmsConfig::from(
                penumbra_keys::test_keys::SPEND_KEY.clone(),
            )),
            governance_custody: None,
        };

        let mut config2 = config.clone();
        config2.custody = CustodyConfig::ViewOnly;
        config2.disable_warning = true;

        let toml_config = toml::to_string_pretty(&config).unwrap();
        let toml_config2 = toml::to_string_pretty(&config2).unwrap();

        println!("{}", toml_config);
        println!("{}", toml_config2);
    }
}