Skip to content

Commit ca010d7

Browse files
feat(general): More types (#217)
* Rename TransactionHash to TransactionId * Add the StakeAddress type * Implement conversion between cardano-blockchain-types::Network and pallas::Network. * Bump versions.
1 parent f76ccf3 commit ca010d7

File tree

12 files changed

+290
-34
lines changed

12 files changed

+290
-34
lines changed

rust/cardano-blockchain-types/Cargo.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "cardano-blockchain-types"
33
description = "Common Cardano Blockchain data types for use in both applications and crates"
44
keywords = ["cardano", "catalyst", ]
5-
version = "0.0.2"
5+
version = "0.0.3"
66
authors = [
77
"Steven Johnson <[email protected]>"
88
]
@@ -20,8 +20,8 @@ workspace = true
2020
[dependencies]
2121
pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" }
2222
# pallas-hardano = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" }
23-
cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" }
24-
catalyst-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" }
23+
cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
24+
catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
2525

2626
ouroboros = "0.18.4"
2727
tracing = "0.1.41"

rust/cardano-blockchain-types/src/hashes.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use catalyst_types::{
66
};
77

88
define_hashes!(
9-
/// A transaction hash - Blake2b-256 hash of a transaction.
10-
(TransactionHash, Blake2b256Hash),
9+
/// A transaction ID - Blake2b-256 hash of a transaction.
10+
(TransactionId, Blake2b256Hash),
1111
/// A public key hash - raw Blake2b-224 hash of an Ed25519 public key (has no discriminator, just the hash).
1212
(PubKeyHash, Blake2b224Hash),
1313
);

rust/cardano-blockchain-types/src/lib.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod multi_era_block_data;
99
mod network;
1010
mod point;
1111
mod slot;
12+
mod stake_address;
1213
mod txn_index;
1314
mod txn_output_offset;
1415
mod txn_witness;
@@ -23,12 +24,13 @@ pub use auxdata::{
2324
};
2425
pub use cip134_uri::Cip0134Uri;
2526
pub use fork::Fork;
26-
pub use hashes::{PubKeyHash, TransactionHash};
27+
pub use hashes::{PubKeyHash, TransactionId};
2728
pub use metadata::cip36::{voting_pk::VotingPubKey, Cip36};
2829
pub use multi_era_block_data::MultiEraBlock;
2930
pub use network::Network;
3031
pub use point::Point;
3132
pub use slot::Slot;
33+
pub use stake_address::StakeAddress;
3234
pub use txn_index::TxnIndex;
3335
pub use txn_output_offset::TxnOutputOffset;
3436
pub use txn_witness::{TxnWitness, VKeyHash};

rust/cardano-blockchain-types/src/network.rs

+23-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
33
use std::{ffi::OsStr, path::PathBuf};
44

5+
use anyhow::anyhow;
56
use catalyst_types::conversion::from_saturating;
67
use chrono::{DateTime, Utc};
78
use pallas::{
8-
ledger::traverse::wellknown::GenesisValues,
9+
ledger::{addresses::Network as PallasNetwork, traverse::wellknown::GenesisValues},
910
network::miniprotocols::{MAINNET_MAGIC, PREVIEW_MAGIC, PRE_PRODUCTION_MAGIC},
1011
};
11-
// use strum::IntoEnumIterator;
12-
// use strum_macros;
1312
use tracing::debug;
1413

1514
use crate::Slot;
@@ -220,6 +219,27 @@ impl From<Network> for u64 {
220219
}
221220
}
222221

222+
impl From<Network> for PallasNetwork {
223+
fn from(value: Network) -> Self {
224+
match value {
225+
Network::Mainnet => PallasNetwork::Mainnet,
226+
Network::Preprod | Network::Preview => PallasNetwork::Testnet,
227+
}
228+
}
229+
}
230+
231+
impl TryFrom<PallasNetwork> for Network {
232+
type Error = anyhow::Error;
233+
234+
fn try_from(value: PallasNetwork) -> Result<Self, Self::Error> {
235+
match value {
236+
PallasNetwork::Mainnet => Ok(Network::Mainnet),
237+
PallasNetwork::Testnet => Ok(Network::Preprod),
238+
n @ PallasNetwork::Other(_) => Err(anyhow!("Unsupported network: {n:?}")),
239+
}
240+
}
241+
}
242+
223243
#[cfg(test)]
224244
mod tests {
225245
use std::str::FromStr;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
//! A stake address.
2+
3+
// cspell: words Scripthash, Keyhash
4+
5+
use std::fmt::{Display, Formatter};
6+
7+
use anyhow::{anyhow, Context};
8+
use pallas::{
9+
crypto::hash::Hash,
10+
ledger::{
11+
addresses::{
12+
ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart,
13+
StakeAddress as PallasStakeAddress,
14+
},
15+
primitives::conway,
16+
},
17+
};
18+
19+
use crate::Network;
20+
21+
/// A stake address.
22+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)]
23+
pub struct StakeAddress(PallasStakeAddress);
24+
25+
impl StakeAddress {
26+
/// Creates a new instance from the given parameters.
27+
#[allow(clippy::expect_used, clippy::missing_panics_doc)]
28+
#[must_use]
29+
pub fn new(network: Network, is_script: bool, hash: Hash<28>) -> Self {
30+
let network = network.into();
31+
// `pallas::StakeAddress` can only be constructed from `ShelleyAddress`, so we are forced
32+
// to create a dummy shelley address. The input hash parameter is used to construct both
33+
// payment and delegation parts, but the payment part isn't used in the stake address
34+
// construction, so it doesn't matter.
35+
let payment = ShelleyPaymentPart::Key(hash);
36+
let delegation = if is_script {
37+
ShelleyDelegationPart::Script(hash)
38+
} else {
39+
ShelleyDelegationPart::Key(hash)
40+
};
41+
let address = ShelleyAddress::new(network, payment, delegation);
42+
// This conversion can only fail if the delegation part isn't key or script, but we know
43+
// it is valid because we construct it just above.
44+
let address = address.try_into().expect("Unexpected delegation part");
45+
Self(address)
46+
}
47+
48+
/// Creates `StakeAddress` from `StakeCredential`.
49+
#[must_use]
50+
pub fn from_stake_cred(network: Network, cred: &conway::StakeCredential) -> Self {
51+
match cred {
52+
conway::StakeCredential::Scripthash(h) => Self::new(network, true, *h),
53+
conway::StakeCredential::AddrKeyhash(h) => Self::new(network, false, *h),
54+
}
55+
}
56+
57+
/// Returns true if it is a script address.
58+
#[must_use]
59+
pub fn is_script(&self) -> bool {
60+
self.0.is_script()
61+
}
62+
}
63+
64+
impl From<PallasStakeAddress> for StakeAddress {
65+
fn from(value: PallasStakeAddress) -> Self {
66+
Self(value)
67+
}
68+
}
69+
70+
impl TryFrom<ShelleyAddress> for StakeAddress {
71+
type Error = anyhow::Error;
72+
73+
fn try_from(value: ShelleyAddress) -> Result<Self, Self::Error> {
74+
let address = PallasStakeAddress::try_from(value.clone())
75+
.with_context(|| format!("Unable to get stake address from {value:?}"))?;
76+
Ok(Self(address))
77+
}
78+
}
79+
80+
impl TryFrom<&[u8]> for StakeAddress {
81+
type Error = anyhow::Error;
82+
83+
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
84+
/// A stake address length in bytes.
85+
const ADDRESS_LENGTH: usize = 29;
86+
/// A hash length in bytes.
87+
const HASH_LENGTH: usize = 28;
88+
89+
let (header, hash) = match bytes {
90+
[header, hash @ ..] if hash.len() == HASH_LENGTH => (header, Hash::<28>::from(hash)),
91+
_ => {
92+
return Err(anyhow!(
93+
"Invalid bytes length: {}, expected {ADDRESS_LENGTH}",
94+
bytes.len()
95+
));
96+
},
97+
};
98+
99+
// The network part stored in the last four bits of the header.
100+
let network = match header & 0b0000_1111 {
101+
0 => Network::Preprod,
102+
1 => Network::Mainnet,
103+
v => return Err(anyhow!("Unexpected network value: {v}, header = {header}")),
104+
};
105+
106+
// The 'type' (stake or script) is stored in the first four bits of the header.
107+
let type_ = header >> 4;
108+
let is_script = match type_ {
109+
0b1110 => false,
110+
0b1111 => true,
111+
v => return Err(anyhow!("Unexpected type value: {v}, header = {header}")),
112+
};
113+
114+
Ok(Self::new(network, is_script, hash))
115+
}
116+
}
117+
118+
/// This conversion returns a 29 bytes value that includes both header and hash.
119+
impl From<StakeAddress> for Vec<u8> {
120+
fn from(value: StakeAddress) -> Self {
121+
value.0.to_vec()
122+
}
123+
}
124+
125+
impl Display for StakeAddress {
126+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127+
// The `to_bech32` implementation returns an error if the network isn't equal to testnet
128+
// or mainnet. We don't allow other networks, so it is safe to unwrap, but just in case
129+
// return a debug representation.
130+
let bech32 = self
131+
.0
132+
.to_bech32()
133+
.unwrap_or_else(|_| format!("{:?}", self.0));
134+
write!(f, "{bech32}")
135+
}
136+
}
137+
138+
#[cfg(test)]
139+
mod tests {
140+
use super::*;
141+
142+
#[allow(clippy::indexing_slicing)]
143+
#[test]
144+
fn roundtrip() {
145+
let hash: Hash<28> = "276fd18711931e2c0e21430192dbeac0e458093cd9d1fcd7210f64b3"
146+
.parse()
147+
.unwrap();
148+
let test_data = [
149+
(Network::Mainnet, true, hash, 0b1111_0001),
150+
(Network::Mainnet, false, hash, 0b1110_0001),
151+
(Network::Preprod, true, hash, 0b1111_0000),
152+
(Network::Preprod, false, hash, 0b1110_0000),
153+
(Network::Preview, true, hash, 0b1111_0000),
154+
(Network::Preview, false, hash, 0b1110_0000),
155+
];
156+
157+
for (network, is_script, hash, expected_header) in test_data {
158+
let stake_address = StakeAddress::new(network, is_script, hash);
159+
assert_eq!(stake_address.is_script(), is_script);
160+
161+
// Check that conversion to bytes includes the expected header value.
162+
let bytes: Vec<_> = stake_address.clone().into();
163+
assert_eq!(29, bytes.len(), "Invalid length for {network} {is_script}");
164+
assert_eq!(
165+
&bytes[1..],
166+
hash.as_ref(),
167+
"Invalid hash for {network} {is_script}"
168+
);
169+
assert_eq!(
170+
expected_header,
171+
*bytes.first().unwrap(),
172+
"Invalid header for {network} {is_script}"
173+
);
174+
175+
// Check that it is possible to create an address from the bytes.
176+
let from_bytes = StakeAddress::try_from(bytes.as_slice()).unwrap();
177+
assert_eq!(from_bytes.is_script(), is_script);
178+
assert_eq!(from_bytes, stake_address);
179+
}
180+
}
181+
182+
#[test]
183+
fn display() {
184+
let hash: Hash<28> = "276fd18711931e2c0e21430192dbeac0e458093cd9d1fcd7210f64b3"
185+
.parse()
186+
.unwrap();
187+
188+
// cSpell:disable
189+
let test_data = [
190+
(
191+
Network::Mainnet,
192+
true,
193+
hash,
194+
"stake17ynkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvcpxcgqv",
195+
),
196+
(
197+
Network::Mainnet,
198+
false,
199+
hash,
200+
"stake1uynkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvcgwyghv",
201+
),
202+
(
203+
Network::Preprod,
204+
true,
205+
hash,
206+
"stake_test17qnkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvcxvj2y3",
207+
),
208+
(
209+
Network::Preprod,
210+
false,
211+
hash,
212+
"stake_test1uqnkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvc0yw2n3",
213+
),
214+
(
215+
Network::Preview,
216+
true,
217+
hash,
218+
"stake_test17qnkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvcxvj2y3",
219+
),
220+
(
221+
Network::Preview,
222+
false,
223+
hash,
224+
"stake_test1uqnkl5v8zxf3utqwy9psrykmatqwgkqf8nvarlxhyy8kfvc0yw2n3",
225+
),
226+
];
227+
// cSpell:enable
228+
229+
for (network, is_script, hash, expected) in test_data {
230+
let address = StakeAddress::new(network, is_script, hash);
231+
assert_eq!(expected, format!("{address}"));
232+
}
233+
}
234+
}

rust/cardano-chain-follower/Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cardano-chain-follower"
3-
version = "0.0.7"
3+
version = "0.0.8"
44
edition.workspace = true
55
authors.workspace = true
66
homepage.workspace = true
@@ -19,8 +19,8 @@ mithril-client = { version = "0.10.4", default-features = false, features = [
1919
"full",
2020
"num-integer-backend",
2121
] }
22-
cardano-blockchain-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" }
23-
catalyst-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" }
22+
cardano-blockchain-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
23+
catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
2424

2525
thiserror = "1.0.69"
2626
tokio = { version = "1.42.0", features = [
@@ -63,7 +63,7 @@ test-log = { version = "0.2.16", default-features = false, features = [
6363
"trace",
6464
] }
6565
clap = "4.5.23"
66-
rbac-registration = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" }
66+
rbac-registration = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
6767

6868
# Note, these features are for support of features exposed by dependencies.
6969
[features]

rust/catalyst-types/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "catalyst-types"
3-
version = "0.0.2"
3+
version = "0.0.3"
44
edition.workspace = true
55
license.workspace = true
66
authors.workspace = true

rust/rbac-registration/Cargo.toml

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "rbac-registration"
33
description = "Role Based Access Control Registration"
44
keywords = ["cardano", "catalyst", "rbac registration"]
5-
version = "0.0.3"
5+
version = "0.0.4"
66
authors = [
77
"Arissara Chotivichit <[email protected]>"
88
]
@@ -30,8 +30,8 @@ tracing = "0.1.40"
3030
ed25519-dalek = "2.1.1"
3131
uuid = "1.11.0"
3232

33-
c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" }
33+
c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
3434
pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" }
35-
cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" }
36-
cardano-blockchain-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" }
37-
catalyst-types = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250218-00" }
35+
cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
36+
cardano-blockchain-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }
37+
catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250220-00" }

0 commit comments

Comments
 (0)