1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc = include_str!("../README.md")]
3#![deny(missing_docs)]
4#![cfg_attr(not(test), no_std)]
5
6use core::fmt::{self, Write};
7extern crate alloc;
8use alloc::{
9 vec,
10 string::{String, ToString},
11};
12
13use zeroize::Zeroize;
14
15use curve25519_dalek::EdwardsPoint;
16
17use monero_io::*;
18
19use monero_base58::{encode_check, decode_check};
20
21#[cfg(test)]
22mod tests;
23
24#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
29pub enum AddressType {
30 Legacy,
32 LegacyIntegrated([u8; 8]),
34 Subaddress,
38 Featured {
47 subaddress: bool,
49 payment_id: Option<[u8; 8]>,
51 guaranteed: bool,
57 },
58}
59
60impl AddressType {
61 pub fn is_subaddress(&self) -> bool {
63 matches!(self, AddressType::Subaddress) ||
64 matches!(self, AddressType::Featured { subaddress: true, .. })
65 }
66
67 pub fn payment_id(&self) -> Option<[u8; 8]> {
69 if let AddressType::LegacyIntegrated(id) = self {
70 Some(*id)
71 } else if let AddressType::Featured { payment_id, .. } = self {
72 *payment_id
73 } else {
74 None
75 }
76 }
77
78 pub fn is_guaranteed(&self) -> bool {
84 matches!(self, AddressType::Featured { guaranteed: true, .. })
85 }
86}
87
88#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
92pub struct SubaddressIndex {
93 account: u32,
94 address: u32,
95}
96
97impl SubaddressIndex {
98 pub const fn new(account: u32, address: u32) -> Option<SubaddressIndex> {
100 if (account == 0) && (address == 0) {
101 return None;
102 }
103 Some(SubaddressIndex { account, address })
104 }
105
106 pub const fn account(&self) -> u32 {
108 self.account
109 }
110
111 pub const fn address(&self) -> u32 {
113 self.address
114 }
115}
116
117#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
121pub struct AddressBytes {
122 legacy: u8,
123 legacy_integrated: u8,
124 subaddress: u8,
125 featured: u8,
126}
127
128impl AddressBytes {
129 pub const fn new(
131 legacy: u8,
132 legacy_integrated: u8,
133 subaddress: u8,
134 featured: u8,
135 ) -> Option<Self> {
136 if (legacy == legacy_integrated) || (legacy == subaddress) || (legacy == featured) {
137 return None;
138 }
139 if (legacy_integrated == subaddress) || (legacy_integrated == featured) {
140 return None;
141 }
142 if subaddress == featured {
143 return None;
144 }
145 Some(AddressBytes { legacy, legacy_integrated, subaddress, featured })
146 }
147
148 const fn to_const_generic(self) -> u32 {
149 ((self.legacy as u32) << 24) +
150 ((self.legacy_integrated as u32) << 16) +
151 ((self.subaddress as u32) << 8) +
152 (self.featured as u32)
153 }
154
155 #[allow(clippy::cast_possible_truncation)]
156 const fn from_const_generic(const_generic: u32) -> Self {
157 let legacy = (const_generic >> 24) as u8;
158 let legacy_integrated = ((const_generic >> 16) & (u8::MAX as u32)) as u8;
159 let subaddress = ((const_generic >> 8) & (u8::MAX as u32)) as u8;
160 let featured = (const_generic & (u8::MAX as u32)) as u8;
161
162 AddressBytes { legacy, legacy_integrated, subaddress, featured }
163 }
164}
165
166const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 70) {
170 Some(bytes) => bytes,
171 None => panic!("mainnet byte constants conflicted"),
172};
173const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) {
176 Some(bytes) => bytes,
177 None => panic!("stagenet byte constants conflicted"),
178};
179const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) {
182 Some(bytes) => bytes,
183 None => panic!("testnet byte constants conflicted"),
184};
185
186#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
188pub enum Network {
189 Mainnet,
191 Stagenet,
195 Testnet,
199}
200
201#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
203pub enum AddressError {
204 #[error("invalid byte for the address's network/type ({0})")]
206 InvalidTypeByte(u8),
207 #[error("invalid address encoding")]
209 InvalidEncoding,
210 #[error("invalid length")]
212 InvalidLength,
213 #[error("invalid key")]
215 InvalidKey,
216 #[error("unknown features")]
218 UnknownFeatures(u64),
219 #[error("different network ({actual:?}) than expected ({expected:?})")]
221 DifferentNetwork {
222 expected: Network,
224 actual: Network,
226 },
227}
228
229#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
233pub struct NetworkedAddressBytes {
234 mainnet: AddressBytes,
235 stagenet: AddressBytes,
236 testnet: AddressBytes,
237}
238
239impl NetworkedAddressBytes {
240 pub const fn new(
242 mainnet: AddressBytes,
243 stagenet: AddressBytes,
244 testnet: AddressBytes,
245 ) -> Option<Self> {
246 let res = NetworkedAddressBytes { mainnet, stagenet, testnet };
247 let all_bytes = res.to_const_generic();
248
249 let mut i = 0;
250 while i < 12 {
251 let this_byte = (all_bytes >> (32 + (i * 8))) & (u8::MAX as u128);
252
253 let mut j = 0;
254 while j < 12 {
255 if i == j {
256 j += 1;
257 continue;
258 }
259 let other_byte = (all_bytes >> (32 + (j * 8))) & (u8::MAX as u128);
260 if this_byte == other_byte {
261 return None;
262 }
263
264 j += 1;
265 }
266
267 i += 1;
268 }
269
270 Some(res)
271 }
272
273 pub const fn to_const_generic(self) -> u128 {
277 ((self.mainnet.to_const_generic() as u128) << 96) +
278 ((self.stagenet.to_const_generic() as u128) << 64) +
279 ((self.testnet.to_const_generic() as u128) << 32)
280 }
281
282 #[allow(clippy::cast_possible_truncation)]
283 const fn from_const_generic(const_generic: u128) -> Self {
284 let mainnet = AddressBytes::from_const_generic((const_generic >> 96) as u32);
285 let stagenet =
286 AddressBytes::from_const_generic(((const_generic >> 64) & (u32::MAX as u128)) as u32);
287 let testnet =
288 AddressBytes::from_const_generic(((const_generic >> 32) & (u32::MAX as u128)) as u32);
289
290 NetworkedAddressBytes { mainnet, stagenet, testnet }
291 }
292
293 fn network(&self, network: Network) -> &AddressBytes {
294 match network {
295 Network::Mainnet => &self.mainnet,
296 Network::Stagenet => &self.stagenet,
297 Network::Testnet => &self.testnet,
298 }
299 }
300
301 fn byte(&self, network: Network, kind: AddressType) -> u8 {
302 let address_bytes = self.network(network);
303
304 match kind {
305 AddressType::Legacy => address_bytes.legacy,
306 AddressType::LegacyIntegrated(_) => address_bytes.legacy_integrated,
307 AddressType::Subaddress => address_bytes.subaddress,
308 AddressType::Featured { .. } => address_bytes.featured,
309 }
310 }
311
312 fn metadata_from_byte(&self, byte: u8) -> Result<(Network, AddressType), AddressError> {
314 let mut meta = None;
315 for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
316 let address_bytes = self.network(network);
317 if let Some(kind) = match byte {
318 _ if byte == address_bytes.legacy => Some(AddressType::Legacy),
319 _ if byte == address_bytes.legacy_integrated => Some(AddressType::LegacyIntegrated([0; 8])),
320 _ if byte == address_bytes.subaddress => Some(AddressType::Subaddress),
321 _ if byte == address_bytes.featured => {
322 Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false })
323 }
324 _ => None,
325 } {
326 meta = Some((network, kind));
327 break;
328 }
329 }
330
331 meta.ok_or(AddressError::InvalidTypeByte(byte))
332 }
333}
334
335pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new(
337 MONERO_MAINNET_BYTES,
338 MONERO_STAGENET_BYTES,
339 MONERO_TESTNET_BYTES,
340) {
341 Some(bytes) => bytes,
342 None => panic!("Monero network byte constants conflicted"),
343};
344
345#[derive(Clone, Copy, PartialEq, Eq, Zeroize)]
347pub struct Address<const ADDRESS_BYTES: u128> {
348 network: Network,
349 kind: AddressType,
350 spend: EdwardsPoint,
351 view: EdwardsPoint,
352}
353
354impl<const ADDRESS_BYTES: u128> fmt::Debug for Address<ADDRESS_BYTES> {
355 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
356 let hex = |bytes: &[u8]| -> Result<String, fmt::Error> {
357 let mut res = String::with_capacity(2 + (2 * bytes.len()));
358 res.push_str("0x");
359 for b in bytes {
360 write!(&mut res, "{b:02x}")?;
361 }
362 Ok(res)
363 };
364
365 fmt
366 .debug_struct("Address")
367 .field("network", &self.network)
368 .field("kind", &self.kind)
369 .field("spend", &hex(&self.spend.compress().to_bytes())?)
370 .field("view", &hex(&self.view.compress().to_bytes())?)
371 .field("(address)", &self.to_string())
373 .finish()
374 }
375}
376
377impl<const ADDRESS_BYTES: u128> fmt::Display for Address<ADDRESS_BYTES> {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 let address_bytes: NetworkedAddressBytes =
380 NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
381
382 let mut data = vec![address_bytes.byte(self.network, self.kind)];
383 data.extend(self.spend.compress().to_bytes());
384 data.extend(self.view.compress().to_bytes());
385 if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.kind {
386 let features_uint =
387 (u8::from(guaranteed) << 2) + (u8::from(payment_id.is_some()) << 1) + u8::from(subaddress);
388 write_varint(&features_uint, &mut data)
389 .expect("write failed but <Vec as io::Write> doesn't fail");
390 }
391 if let Some(id) = self.kind.payment_id() {
392 data.extend(id);
393 }
394 write!(f, "{}", encode_check(data))
395 }
396}
397
398impl<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
399 pub fn new(network: Network, kind: AddressType, spend: EdwardsPoint, view: EdwardsPoint) -> Self {
401 Address { network, kind, spend, view }
402 }
403
404 pub fn from_str_with_unchecked_network(s: &str) -> Result<Self, AddressError> {
406 let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?;
407 let mut raw = raw.as_slice();
408
409 let address_bytes: NetworkedAddressBytes =
410 NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
411 let (network, mut kind) = address_bytes
412 .metadata_from_byte(read_byte(&mut raw).map_err(|_| AddressError::InvalidLength)?)?;
413 let spend = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
414 let view = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
415
416 if matches!(kind, AddressType::Featured { .. }) {
417 let features = read_varint::<_, u64>(&mut raw).map_err(|_| AddressError::InvalidLength)?;
418 if (features >> 3) != 0 {
419 Err(AddressError::UnknownFeatures(features))?;
420 }
421
422 let subaddress = (features & 1) == 1;
423 let integrated = ((features >> 1) & 1) == 1;
424 let guaranteed = ((features >> 2) & 1) == 1;
425
426 kind =
427 AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed };
428 }
429
430 match kind {
432 AddressType::LegacyIntegrated(ref mut id) |
433 AddressType::Featured { payment_id: Some(ref mut id), .. } => {
434 *id = read_bytes(&mut raw).map_err(|_| AddressError::InvalidLength)?;
435 }
436 _ => {}
437 };
438
439 if !raw.is_empty() {
440 Err(AddressError::InvalidLength)?;
441 }
442
443 Ok(Address { network, kind, spend, view })
444 }
445
446 pub fn from_str(network: Network, s: &str) -> Result<Self, AddressError> {
451 Self::from_str_with_unchecked_network(s).and_then(|addr| {
452 if addr.network == network {
453 Ok(addr)
454 } else {
455 Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })?
456 }
457 })
458 }
459
460 pub fn network(&self) -> Network {
462 self.network
463 }
464
465 pub fn kind(&self) -> &AddressType {
467 &self.kind
468 }
469
470 pub fn is_subaddress(&self) -> bool {
472 self.kind.is_subaddress()
473 }
474
475 pub fn payment_id(&self) -> Option<[u8; 8]> {
477 self.kind.payment_id()
478 }
479
480 pub fn is_guaranteed(&self) -> bool {
486 self.kind.is_guaranteed()
487 }
488
489 pub fn spend(&self) -> EdwardsPoint {
491 self.spend
492 }
493
494 pub fn view(&self) -> EdwardsPoint {
496 self.view
497 }
498}
499
500pub type MoneroAddress = Address<{ MONERO_BYTES.to_const_generic() }>;