dpp/address_funds/
orchard_address.rs1use bech32::{Bech32m, Hrp};
2use dashcore::Network;
3
4use crate::address_funds::platform_address::{PLATFORM_HRP_MAINNET, PLATFORM_HRP_TESTNET};
5use crate::address_funds::PlatformAddress;
6use crate::ProtocolError;
7
8pub const ORCHARD_DIVERSIFIER_SIZE: usize = 11;
10pub const ORCHARD_PKD_SIZE: usize = 32;
12pub const ORCHARD_ADDRESS_SIZE: usize = ORCHARD_DIVERSIFIER_SIZE + ORCHARD_PKD_SIZE;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct OrchardAddress(grovedb_commitment_tree::PaymentAddress);
34
35impl OrchardAddress {
36 pub const ORCHARD_TYPE: u8 = 0x10;
39
40 pub fn inner(&self) -> &grovedb_commitment_tree::PaymentAddress {
42 &self.0
43 }
44
45 pub fn into_inner(self) -> grovedb_commitment_tree::PaymentAddress {
47 self.0
48 }
49
50 pub fn from_raw_bytes(bytes: &[u8; ORCHARD_ADDRESS_SIZE]) -> Result<Self, ProtocolError> {
55 let addr =
56 Option::from(grovedb_commitment_tree::PaymentAddress::from_raw_address_bytes(bytes))
57 .ok_or_else(|| {
58 ProtocolError::DecodingError(
59 "OrchardAddress pk_d is not a valid Pallas curve point".to_string(),
60 )
61 })?;
62 Ok(Self(addr))
63 }
64
65 pub fn to_raw_bytes(&self) -> [u8; ORCHARD_ADDRESS_SIZE] {
67 self.0.to_raw_address_bytes()
68 }
69
70 pub fn to_bech32m_string(&self, network: Network) -> String {
77 let hrp_str = PlatformAddress::hrp_for_network(network);
78 let hrp = Hrp::parse(hrp_str).expect("HRP is valid");
79
80 let raw = self.to_raw_bytes();
81 let mut payload = Vec::with_capacity(1 + ORCHARD_ADDRESS_SIZE);
82 payload.push(Self::ORCHARD_TYPE);
83 payload.extend_from_slice(&raw);
84
85 bech32::encode::<Bech32m>(hrp, &payload).expect("encoding should succeed")
86 }
87
88 pub fn from_bech32m_string(s: &str) -> Result<(Self, Network), ProtocolError> {
94 let (hrp, data) =
95 bech32::decode(s).map_err(|e| ProtocolError::DecodingError(format!("{}", e)))?;
96
97 let hrp_lower = hrp.as_str().to_ascii_lowercase();
98 let network = match hrp_lower.as_str() {
99 s if s == PLATFORM_HRP_MAINNET => Network::Mainnet,
100 s if s == PLATFORM_HRP_TESTNET => Network::Testnet,
101 _ => {
102 return Err(ProtocolError::DecodingError(format!(
103 "invalid HRP '{}': expected '{}' or '{}'",
104 hrp, PLATFORM_HRP_MAINNET, PLATFORM_HRP_TESTNET
105 )))
106 }
107 };
108
109 if data.len() != 1 + ORCHARD_ADDRESS_SIZE {
111 return Err(ProtocolError::DecodingError(format!(
112 "invalid Orchard address length: expected {} bytes, got {}",
113 1 + ORCHARD_ADDRESS_SIZE,
114 data.len()
115 )));
116 }
117
118 if data[0] != Self::ORCHARD_TYPE {
119 return Err(ProtocolError::DecodingError(format!(
120 "invalid Orchard address type byte: expected 0x{:02x}, got 0x{:02x}",
121 Self::ORCHARD_TYPE,
122 data[0]
123 )));
124 }
125
126 let mut raw = [0u8; ORCHARD_ADDRESS_SIZE];
127 raw.copy_from_slice(&data[1..]);
128 Self::from_raw_bytes(&raw).map(|addr| (addr, network))
129 }
130}
131
132impl From<grovedb_commitment_tree::PaymentAddress> for OrchardAddress {
134 fn from(addr: grovedb_commitment_tree::PaymentAddress) -> Self {
135 Self(addr)
136 }
137}
138
139impl From<&grovedb_commitment_tree::PaymentAddress> for OrchardAddress {
141 fn from(addr: &grovedb_commitment_tree::PaymentAddress) -> Self {
142 Self(*addr)
143 }
144}
145
146impl std::fmt::Display for OrchardAddress {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 let raw = self.to_raw_bytes();
149 write!(
150 f,
151 "Orchard(d={}, pk_d={})",
152 hex::encode(&raw[..ORCHARD_DIVERSIFIER_SIZE]),
153 hex::encode(&raw[ORCHARD_DIVERSIFIER_SIZE..])
154 )
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use bech32::Hrp;
162
163 fn test_orchard_address() -> OrchardAddress {
164 use grovedb_commitment_tree::{FullViewingKey, Scope, SpendingKey};
165 let sk = SpendingKey::from_bytes([42u8; 32]).unwrap();
166 let fvk = FullViewingKey::from(&sk);
167 let payment_address = fvk.address_at(0u32, Scope::External);
168 OrchardAddress::from(payment_address)
169 }
170
171 #[test]
172 fn test_orchard_address_raw_bytes_roundtrip() {
173 let address = test_orchard_address();
174 let raw = address.to_raw_bytes();
175 assert_eq!(raw.len(), 43);
176
177 let recovered = OrchardAddress::from_raw_bytes(&raw).unwrap();
178 assert_eq!(recovered, address);
179 }
180
181 #[test]
182 fn test_orchard_bech32m_mainnet_roundtrip() {
183 let address = test_orchard_address();
184
185 let encoded = address.to_bech32m_string(Network::Mainnet);
186 assert!(
187 encoded.starts_with("dash1z"),
188 "Orchard mainnet address should start with 'dash1z', got: {}",
189 encoded
190 );
191
192 let (decoded, network) =
193 OrchardAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
194 assert_eq!(decoded, address);
195 assert_eq!(network, Network::Mainnet);
196 }
197
198 #[test]
199 fn test_orchard_bech32m_testnet_roundtrip() {
200 let address = test_orchard_address();
201
202 let encoded = address.to_bech32m_string(Network::Testnet);
203 assert!(
204 encoded.starts_with("tdash1z"),
205 "Orchard testnet address should start with 'tdash1z', got: {}",
206 encoded
207 );
208
209 let (decoded, network) =
210 OrchardAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
211 assert_eq!(decoded, address);
212 assert_eq!(network, Network::Testnet);
213 }
214
215 #[test]
216 fn test_orchard_bech32m_wrong_type_byte_fails() {
217 let hrp = Hrp::parse("dash").unwrap();
219 let mut payload = vec![PlatformAddress::P2PKH_TYPE]; payload.extend_from_slice(&[0u8; 43]);
221 let encoded = bech32::encode::<Bech32m>(hrp, &payload).unwrap();
222
223 let result = OrchardAddress::from_bech32m_string(&encoded);
224 assert!(result.is_err());
225 assert!(result
226 .unwrap_err()
227 .to_string()
228 .contains("invalid Orchard address type byte"));
229 }
230
231 #[test]
232 fn test_orchard_bech32m_wrong_length_fails() {
233 let hrp = Hrp::parse("dash").unwrap();
235 let mut payload = vec![OrchardAddress::ORCHARD_TYPE];
236 payload.extend_from_slice(&[0u8; 20]);
237 let encoded = bech32::encode::<Bech32m>(hrp, &payload).unwrap();
238
239 let result = OrchardAddress::from_bech32m_string(&encoded);
240 assert!(result.is_err());
241 assert!(result
242 .unwrap_err()
243 .to_string()
244 .contains("invalid Orchard address length"));
245 }
246
247 #[test]
248 fn test_orchard_and_platform_addresses_are_distinguishable() {
249 let p2pkh = PlatformAddress::P2pkh([0xAB; 20]);
250 let p2sh = PlatformAddress::P2sh([0xAB; 20]);
251 let orchard = test_orchard_address();
252
253 let p2pkh_enc = p2pkh.to_bech32m_string(Network::Mainnet);
254 let p2sh_enc = p2sh.to_bech32m_string(Network::Mainnet);
255 let orchard_enc = orchard.to_bech32m_string(Network::Mainnet);
256
257 assert!(p2pkh_enc.starts_with("dash1k"), "P2PKH: {}", p2pkh_enc);
259 assert!(p2sh_enc.starts_with("dash1s"), "P2SH: {}", p2sh_enc);
260 assert!(
261 orchard_enc.starts_with("dash1z"),
262 "Orchard: {}",
263 orchard_enc
264 );
265
266 assert!(PlatformAddress::from_bech32m_string(&orchard_enc).is_err());
268 assert!(OrchardAddress::from_bech32m_string(&p2pkh_enc).is_err());
269 }
270
271 #[test]
272 fn test_orchard_address_from_raw_bytes_invalid_pk_d() {
273 let mut raw = [0u8; 43];
275 raw[0] = 0x01; assert!(OrchardAddress::from_raw_bytes(&raw).is_err());
277 }
278
279 #[test]
280 fn test_orchard_address_display() {
281 let address = test_orchard_address();
282 let display = format!("{}", address);
283 assert!(display.starts_with("Orchard(d="));
284 assert!(display.contains("pk_d="));
285 }
286}