cardano_sdk/cardano/
credential.rs1use crate::{Hash, VerificationKey, WithNetworkId, cbor, pallas};
6use anyhow::anyhow;
7use std::{fmt, str::FromStr};
8
9#[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
59impl 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 pub fn from_key(hash: Hash<28>) -> Self {
76 Self::from(pallas::StakeCredential::AddrKeyhash(pallas::Hash::from(
77 hash,
78 )))
79 }
80
81 pub fn from_script(hash: Hash<28>) -> Self {
85 Self::from(pallas::StakeCredential::ScriptHash(pallas::Hash::from(
86 hash,
87 )))
88 }
89}
90
91impl Credential {
94 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 pub fn as_key(&self) -> Option<Hash<28>> {
134 self.select(Some, |_| None)
135 }
136
137 pub fn as_script(&self) -> Option<Hash<28>> {
139 self.select(|_| None, Some)
140 }
141}
142
143impl 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
211impl 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 #[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 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 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}