cardano_sdk/cardano/
credential.rs

1//  This Source Code Form is subject to the terms of the Mozilla Public
2//  License, v. 2.0. If a copy of the MPL was not distributed with this
3//  file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use crate::{Hash, VerificationKey, WithNetworkId, cbor, pallas};
6use anyhow::anyhow;
7use std::{fmt, str::FromStr};
8
9/// A wrapper around the _blake2b-224_ hash digest of a key or script.
10///
11/// It behaves like a enum with two variants, although the constructors are kept private to avoid
12/// leaking implementation internals. One can manipulate either of the two variants by using the
13/// higher-level API:
14///
15/// - [`Self::as_key`]
16/// - [`Self::as_script`]
17///
18/// If something more fine-grained is needed where either are needed, one may simply use:
19///
20/// - [`Self::select`]
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, cbor::Encode, cbor::Decode)]
22#[repr(transparent)]
23#[cbor(transparent)]
24pub struct Credential(#[n(0)] pallas::StakeCredential);
25
26impl fmt::Display for Credential {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
28        f.write_str(
29            self.select(
30                |hash| format!("Key({hash})"),
31                |hash| format!("Script({hash})"),
32            )
33            .as_str(),
34        )
35    }
36}
37
38impl fmt::Display for WithNetworkId<'_, Credential> {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
40        let addr_type = self.inner.select(|_| 0b1110, |_| 0b1111) << 4;
41        let header = addr_type | u8::from(self.network_id);
42        let payload = [&[header], Hash::from(self.inner).as_ref()].concat();
43        f.write_str(
44            bech32::encode(
45                if self.network_id.is_mainnet() {
46                    "stake"
47                } else {
48                    "stake_test"
49                },
50                bech32::ToBase32::to_base32(&payload),
51                bech32::Variant::Bech32,
52            )
53            .expect("invalid bech32 string")
54            .as_str(),
55        )
56    }
57}
58
59// -------------------------------------------------------------------- Building
60
61impl Default for Credential {
62    fn default() -> Self {
63        Self::from_key(Hash::from([
64            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
65        ]))
66    }
67}
68
69impl Credential {
70    pub const DIGEST_SIZE: usize = 28;
71
72    /// Construct a credential from a key.
73    ///
74    /// See also [`key_credential!`](crate::key_credential).
75    pub fn from_key(hash: Hash<28>) -> Self {
76        Self::from(pallas::StakeCredential::AddrKeyhash(pallas::Hash::from(
77            hash,
78        )))
79    }
80
81    /// Construct a credential from a script.
82    ///
83    /// See also [`script_credential!`](crate::script_credential).
84    pub fn from_script(hash: Hash<28>) -> Self {
85        Self::from(pallas::StakeCredential::ScriptHash(pallas::Hash::from(
86            hash,
87        )))
88    }
89}
90
91// ------------------------------------------------------------------ Inspecting
92
93impl Credential {
94    /// Run a computation (possibly the same) for either of the two variants.
95    ///
96    /// # examples
97    ///
98    /// ```rust
99    /// # use cardano_sdk::{script_credential};
100    /// assert_eq!(
101    ///   script_credential!("bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777")
102    ///     .select(
103    ///         |_| "is_key".to_string(),
104    ///         |_| "is_script".to_string(),
105    ///     ),
106    ///   "is_script"
107    /// );
108    /// ```
109    ///
110    /// ```rust
111    /// # use cardano_sdk::{key_credential};
112    /// assert_eq!(
113    ///   key_credential!("bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777")
114    ///     .select(
115    ///         |hash| format!("Key({hash})"),
116    ///         |hash| format!("Script({hash})"),
117    ///     ),
118    ///   "Key(bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777)"
119    /// )
120    /// ```
121    pub fn select<T>(
122        &self,
123        mut when_key: impl FnMut(Hash<28>) -> T,
124        mut when_script: impl FnMut(Hash<28>) -> T,
125    ) -> T {
126        match &self.0 {
127            pallas::StakeCredential::AddrKeyhash(hash) => when_key(Hash::from(hash)),
128            pallas::StakeCredential::ScriptHash(hash) => when_script(Hash::from(hash)),
129        }
130    }
131
132    /// Continues with the inner hash, provided that the credential is that of a key.
133    pub fn as_key(&self) -> Option<Hash<28>> {
134        self.select(Some, |_| None)
135    }
136
137    /// Continues with the inner hash, provided that the credential is that of a script.
138    pub fn as_script(&self) -> Option<Hash<28>> {
139        self.select(|_| None, Some)
140    }
141}
142
143// ----------------------------------------------------------- Converting (from)
144
145impl FromStr for Credential {
146    type Err = anyhow::Error;
147    fn from_str(s: &str) -> anyhow::Result<Self> {
148        match pallas::Address::from_bech32(s)? {
149            pallas::Address::Stake(stake) => Ok(Self::from(stake.payload())),
150            pallas::Address::Byron { .. } | pallas::Address::Shelley { .. } => {
151                Err(anyhow!("invalid stake address type"))
152            }
153        }
154    }
155}
156
157impl From<&pallas::StakePayload> for Credential {
158    fn from(stake_payload: &pallas::StakePayload) -> Self {
159        match stake_payload {
160            pallas::StakePayload::Stake(hash) => Self::from_key(Hash::from(hash)),
161            pallas::StakePayload::Script(hash) => Self::from_script(Hash::from(hash)),
162        }
163    }
164}
165
166impl From<pallas::StakeCredential> for Credential {
167    fn from(credential: pallas::StakeCredential) -> Self {
168        Self(credential)
169    }
170}
171
172impl From<&pallas::ShelleyPaymentPart> for Credential {
173    fn from(payment_part: &pallas::ShelleyPaymentPart) -> Self {
174        match payment_part {
175            pallas_addresses::ShelleyPaymentPart::Key(hash) => {
176                Self(pallas::StakeCredential::AddrKeyhash(*hash))
177            }
178            pallas_addresses::ShelleyPaymentPart::Script(hash) => {
179                Self(pallas::StakeCredential::ScriptHash(*hash))
180            }
181        }
182    }
183}
184
185impl From<&VerificationKey> for Credential {
186    fn from(key: &VerificationKey) -> Self {
187        Self::from_key(Hash::<28>::new(key))
188    }
189}
190
191impl TryFrom<&pallas::ShelleyDelegationPart> for Credential {
192    type Error = anyhow::Error;
193
194    fn try_from(delegation_part: &pallas::ShelleyDelegationPart) -> anyhow::Result<Self> {
195        match delegation_part {
196            pallas_addresses::ShelleyDelegationPart::Key(hash) => {
197                Ok(Self(pallas::StakeCredential::AddrKeyhash(*hash)))
198            }
199            pallas_addresses::ShelleyDelegationPart::Script(hash) => {
200                Ok(Self(pallas::StakeCredential::ScriptHash(*hash)))
201            }
202            pallas_addresses::ShelleyDelegationPart::Pointer(..) => {
203                Err(anyhow!("unsupported pointer address")
204                    .context(format!("delegation part={:?}", delegation_part)))
205            }
206            pallas_addresses::ShelleyDelegationPart::Null => Err(anyhow!("no delegation part")),
207        }
208    }
209}
210
211// ------------------------------------------------------------- Converting (to)
212
213impl From<Credential> for pallas::StakeCredential {
214    fn from(credential: Credential) -> Self {
215        credential.0
216    }
217}
218
219impl From<Credential> for pallas::ShelleyPaymentPart {
220    fn from(credential: Credential) -> Self {
221        match credential.0 {
222            pallas::StakeCredential::AddrKeyhash(hash) => pallas::ShelleyPaymentPart::Key(hash),
223            pallas::StakeCredential::ScriptHash(hash) => pallas::ShelleyPaymentPart::Script(hash),
224        }
225    }
226}
227
228impl From<Credential> for pallas::ShelleyDelegationPart {
229    fn from(credential: Credential) -> Self {
230        match credential.0 {
231            pallas::StakeCredential::AddrKeyhash(hash) => pallas::ShelleyDelegationPart::Key(hash),
232            pallas::StakeCredential::ScriptHash(hash) => {
233                pallas::ShelleyDelegationPart::Script(hash)
234            }
235        }
236    }
237}
238
239impl From<&Credential> for [u8; 28] {
240    fn from(credential: &Credential) -> Self {
241        match credential.0 {
242            pallas::StakeCredential::AddrKeyhash(hash)
243            | pallas::StakeCredential::ScriptHash(hash) => <[u8; 28]>::try_from(hash.to_vec())
244                .unwrap_or_else(|e| {
245                    unreachable!("Hash<28> held something else than 28 bytes: {e:?}")
246                }),
247        }
248    }
249}
250
251impl From<&Credential> for Hash<28> {
252    fn from(credential: &Credential) -> Self {
253        Hash::from(<[u8; 28]>::from(credential))
254    }
255}
256
257#[cfg(any(test, feature = "test-utils"))]
258pub mod tests {
259    use crate::{Credential, any, key_credential, pallas, script_credential};
260    use proptest::prelude::*;
261
262    // -------------------------------------------------------------- Unit tests
263
264    #[test]
265    fn display_key() {
266        assert_eq!(
267            key_credential!("bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777").to_string(),
268            "Key(bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777)",
269        );
270    }
271
272    #[test]
273    fn display_script() {
274        assert_eq!(
275            script_credential!("bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777")
276                .to_string(),
277            "Script(bd3ae991b5aafccafe5ca70758bd36a9b2f872f57f6d3a1ffa0eb777)",
278        );
279    }
280
281    // -------------------------------------------------------------- Properties
282
283    proptest! {
284        #[test]
285        fn pallas_roundtrip(credential in any::credential()) {
286            let pallas_credential = pallas::StakeCredential::from(credential.clone());
287            let credential_back = Credential::from(pallas_credential);
288            prop_assert_eq!(credential, credential_back);
289        }
290    }
291
292    proptest! {
293        #[test]
294        fn from_key_roundtrip(hash in any::hash28()) {
295            prop_assert!(
296                Credential::from_key(hash)
297                    .as_key()
298                    .is_some_and(|inner_hash| inner_hash == hash)
299            )
300        }
301    }
302
303    proptest! {
304        #[test]
305        fn from_script_roundtrip(hash in any::hash28()) {
306            prop_assert!(
307                Credential::from_script(hash)
308                    .as_script()
309                    .is_some_and(|inner_hash| inner_hash == hash)
310            )
311        }
312    }
313
314    // -------------------------------------------------------------- Generators
315
316    pub mod generators {
317        use super::*;
318
319        pub fn credential() -> impl Strategy<Value = Credential> {
320            prop_oneof![
321                any::hash28().prop_map(Credential::from_key),
322                any::hash28().prop_map(Credential::from_script),
323            ]
324        }
325    }
326}