1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![cfg_attr(not(test), no_std)]
4
5use core::fmt::{self, Write as _};
6extern crate alloc;
7use alloc::{
8 vec,
9 string::{String, ToString as _},
10};
11
12use zeroize::Zeroize;
13
14use monero_io::*;
15use monero_ed25519::{Point, CompressedPoint};
16use monero_primitives::UpperBound;
17
18use monero_base58::{encode_check, decode_check};
19
20#[cfg(test)]
21mod tests;
22
23#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
28pub enum AddressType {
29 Legacy,
31 LegacyIntegrated([u8; 8]),
33 Subaddress,
37 Featured {
46 subaddress: bool,
48 payment_id: Option<[u8; 8]>,
50 guaranteed: bool,
56 },
57}
58
59impl AddressType {
60 pub fn is_subaddress(&self) -> bool {
62 matches!(self, AddressType::Subaddress) ||
63 matches!(self, AddressType::Featured { subaddress: true, .. })
64 }
65
66 pub fn payment_id(&self) -> Option<[u8; 8]> {
68 if let AddressType::LegacyIntegrated(id) = self {
69 Some(*id)
70 } else if let AddressType::Featured { payment_id, .. } = self {
71 *payment_id
72 } else {
73 None
74 }
75 }
76
77 pub fn is_guaranteed(&self) -> bool {
83 matches!(self, AddressType::Featured { guaranteed: true, .. })
84 }
85}
86
87#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
91pub struct SubaddressIndex {
92 account: u32,
93 address: u32,
94}
95
96impl SubaddressIndex {
97 pub const fn new(account: u32, address: u32) -> Option<SubaddressIndex> {
99 if (account == 0) && (address == 0) {
100 return None;
101 }
102 Some(SubaddressIndex { account, address })
103 }
104
105 pub const fn account(&self) -> u32 {
107 self.account
108 }
109
110 pub const fn address(&self) -> u32 {
112 self.address
113 }
114}
115
116#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
120pub struct AddressBytes {
121 legacy: u8,
122 legacy_integrated: u8,
123 subaddress: u8,
124 featured: u8,
125}
126
127impl AddressBytes {
128 pub const fn new(
130 legacy: u8,
131 legacy_integrated: u8,
132 subaddress: u8,
133 featured: u8,
134 ) -> Option<Self> {
135 if (legacy == legacy_integrated) || (legacy == subaddress) || (legacy == featured) {
136 return None;
137 }
138 if (legacy_integrated == subaddress) || (legacy_integrated == featured) {
139 return None;
140 }
141 if subaddress == featured {
142 return None;
143 }
144 Some(AddressBytes { legacy, legacy_integrated, subaddress, featured })
145 }
146
147 #[expect(clippy::as_conversions)]
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 #[expect(clippy::as_conversions, 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 #[error("address's type does not support integrated payment ID")]
229 PaymentIDsUnsupported,
230}
231
232#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
236pub struct NetworkedAddressBytes {
237 mainnet: AddressBytes,
238 stagenet: AddressBytes,
239 testnet: AddressBytes,
240}
241
242impl NetworkedAddressBytes {
243 #[expect(clippy::as_conversions)] pub const fn new(
246 mainnet: AddressBytes,
247 stagenet: AddressBytes,
248 testnet: AddressBytes,
249 ) -> Option<Self> {
250 let res = NetworkedAddressBytes { mainnet, stagenet, testnet };
251 let all_bytes = res.to_const_generic();
252
253 let mut i = 0;
254 while i < 12 {
255 let this_byte = (all_bytes >> (32 + (i * 8))) & (u8::MAX as u128);
256
257 let mut j = 0;
258 while j < 12 {
259 if i == j {
260 j += 1;
261 continue;
262 }
263 let other_byte = (all_bytes >> (32 + (j * 8))) & (u8::MAX as u128);
264 if this_byte == other_byte {
265 return None;
266 }
267
268 j += 1;
269 }
270
271 i += 1;
272 }
273
274 Some(res)
275 }
276
277 #[expect(clippy::as_conversions)]
281 pub const fn to_const_generic(self) -> u128 {
282 ((self.mainnet.to_const_generic() as u128) << 96) +
283 ((self.stagenet.to_const_generic() as u128) << 64) +
284 ((self.testnet.to_const_generic() as u128) << 32)
285 }
286
287 #[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
288 const fn from_const_generic(const_generic: u128) -> Self {
289 let mainnet = AddressBytes::from_const_generic((const_generic >> 96) as u32);
290 let stagenet =
291 AddressBytes::from_const_generic(((const_generic >> 64) & (u32::MAX as u128)) as u32);
292 let testnet =
293 AddressBytes::from_const_generic(((const_generic >> 32) & (u32::MAX as u128)) as u32);
294
295 NetworkedAddressBytes { mainnet, stagenet, testnet }
296 }
297
298 fn network(&self, network: Network) -> &AddressBytes {
299 match network {
300 Network::Mainnet => &self.mainnet,
301 Network::Stagenet => &self.stagenet,
302 Network::Testnet => &self.testnet,
303 }
304 }
305
306 fn byte(&self, network: Network, kind: AddressType) -> u8 {
307 let address_bytes = self.network(network);
308
309 match kind {
310 AddressType::Legacy => address_bytes.legacy,
311 AddressType::LegacyIntegrated(_) => address_bytes.legacy_integrated,
312 AddressType::Subaddress => address_bytes.subaddress,
313 AddressType::Featured { .. } => address_bytes.featured,
314 }
315 }
316
317 fn metadata_from_byte(&self, byte: u8) -> Result<(Network, AddressType), AddressError> {
319 let mut meta = None;
320 for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
321 let address_bytes = self.network(network);
322 if let Some(kind) = match byte {
323 _ if byte == address_bytes.legacy => Some(AddressType::Legacy),
324 _ if byte == address_bytes.legacy_integrated => Some(AddressType::LegacyIntegrated([0; 8])),
325 _ if byte == address_bytes.subaddress => Some(AddressType::Subaddress),
326 _ if byte == address_bytes.featured => {
327 Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false })
328 }
329 _ => None,
330 } {
331 meta = Some((network, kind));
332 break;
333 }
334 }
335
336 meta.ok_or(AddressError::InvalidTypeByte(byte))
337 }
338}
339
340pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new(
342 MONERO_MAINNET_BYTES,
343 MONERO_STAGENET_BYTES,
344 MONERO_TESTNET_BYTES,
345) {
346 Some(bytes) => bytes,
347 None => panic!("Monero network byte constants conflicted"),
348};
349
350#[derive(Clone, Copy, PartialEq, Eq, Zeroize)]
352pub struct Address<const ADDRESS_BYTES: u128> {
353 network: Network,
354 kind: AddressType,
355 spend: Point,
356 view: Point,
357}
358
359impl<const ADDRESS_BYTES: u128> fmt::Debug for Address<ADDRESS_BYTES> {
360 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
361 let hex = |bytes: &[u8]| -> Result<String, fmt::Error> {
362 let mut res = String::with_capacity(2 + (2 * bytes.len()));
363 res.push_str("0x");
364 for b in bytes {
365 write!(&mut res, "{b:02x}")?;
366 }
367 Ok(res)
368 };
369
370 fmt
371 .debug_struct("Address")
372 .field("network", &self.network)
373 .field("kind", &self.kind)
374 .field("spend", &hex(&self.spend.compress().to_bytes())?)
375 .field("view", &hex(&self.view.compress().to_bytes())?)
376 .field("(address)", &self.to_string())
378 .finish()
379 }
380}
381
382impl<const ADDRESS_BYTES: u128> fmt::Display for Address<ADDRESS_BYTES> {
383 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384 let address_bytes: NetworkedAddressBytes =
385 NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
386
387 let mut data = vec![address_bytes.byte(self.network, self.kind)];
388 data.extend(self.spend.compress().to_bytes());
389 data.extend(self.view.compress().to_bytes());
390 if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.kind {
391 let features_uint =
392 (u8::from(guaranteed) << 2) + (u8::from(payment_id.is_some()) << 1) + u8::from(subaddress);
393 VarInt::write(&features_uint, &mut data)
394 .expect("write failed but <Vec as io::Write> doesn't fail");
395 }
396 if let Some(id) = self.kind.payment_id() {
397 data.extend(id);
398 }
399 write!(f, "{}", encode_check(data))
400 }
401}
402
403impl<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
404 const BASE_256_UPPER_BOUND: UpperBound<usize> =
406 UpperBound(<u64 as VarInt>::UPPER_BOUND + 32 + 32 + <u64 as VarInt>::UPPER_BOUND + 8);
407 const CHECKSUMMED_UPPER_BOUND: UpperBound<usize> = UpperBound(Self::BASE_256_UPPER_BOUND.0 + 4);
409 pub const SIZE_UPPER_BOUND: UpperBound<usize> =
413 UpperBound((Self::CHECKSUMMED_UPPER_BOUND.0 * 8).div_ceil(5));
414
415 pub fn new(network: Network, kind: AddressType, spend: Point, view: Point) -> Self {
417 Address { network, kind, spend, view }
418 }
419
420 pub fn from_str_with_unchecked_network(s: &str) -> Result<Self, AddressError> {
422 let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?;
423 let mut raw = raw.as_slice();
424
425 let address_bytes: NetworkedAddressBytes =
426 NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
427 let (network, mut kind) = address_bytes
428 .metadata_from_byte(read_byte(&mut raw).map_err(|_| AddressError::InvalidLength)?)?;
429 let spend = CompressedPoint::read(&mut raw)
430 .ok()
431 .as_ref()
432 .and_then(CompressedPoint::decompress)
433 .ok_or(AddressError::InvalidKey)?;
434 let view = CompressedPoint::read(&mut raw)
435 .ok()
436 .as_ref()
437 .and_then(CompressedPoint::decompress)
438 .ok_or(AddressError::InvalidKey)?;
439
440 if matches!(kind, AddressType::Featured { .. }) {
441 let features = <u64 as VarInt>::read(&mut raw).map_err(|_| AddressError::InvalidLength)?;
442 if (features >> 3) != 0 {
443 Err(AddressError::UnknownFeatures(features))?;
444 }
445
446 let subaddress = (features & 1) == 1;
447 let integrated = ((features >> 1) & 1) == 1;
448 let guaranteed = ((features >> 2) & 1) == 1;
449
450 kind =
451 AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed };
452 }
453
454 match kind {
456 AddressType::LegacyIntegrated(ref mut id) |
457 AddressType::Featured { payment_id: Some(ref mut id), .. } => {
458 *id = read_bytes(&mut raw).map_err(|_| AddressError::InvalidLength)?;
459 }
460 AddressType::Legacy |
461 AddressType::Subaddress |
462 AddressType::Featured { payment_id: None, .. } => {}
463 }
464
465 if !raw.is_empty() {
466 Err(AddressError::InvalidLength)?;
467 }
468
469 Ok(Address { network, kind, spend, view })
470 }
471
472 pub fn from_str(network: Network, s: &str) -> Result<Self, AddressError> {
477 Self::from_str_with_unchecked_network(s).and_then(|addr| {
478 if addr.network == network {
479 Ok(addr)
480 } else {
481 Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })?
482 }
483 })
484 }
485
486 pub fn with_payment_id(&self, payment_id: [u8; 8]) -> Result<Self, AddressError> {
494 match self.kind {
495 AddressType::Legacy | AddressType::LegacyIntegrated(_) => Ok(Self::new(
496 self.network,
497 AddressType::LegacyIntegrated(payment_id),
498 self.spend,
499 self.view,
500 )),
501 AddressType::Subaddress => Err(AddressError::PaymentIDsUnsupported),
502 AddressType::Featured { subaddress, payment_id: _, guaranteed } => Ok(Self::new(
503 self.network,
504 AddressType::Featured { subaddress, payment_id: Some(payment_id), guaranteed },
505 self.spend,
506 self.view,
507 )),
508 }
509 }
510
511 pub fn network(&self) -> Network {
513 self.network
514 }
515
516 pub fn kind(&self) -> &AddressType {
518 &self.kind
519 }
520
521 pub fn is_subaddress(&self) -> bool {
523 self.kind.is_subaddress()
524 }
525
526 pub fn payment_id(&self) -> Option<[u8; 8]> {
528 self.kind.payment_id()
529 }
530
531 pub fn is_guaranteed(&self) -> bool {
537 self.kind.is_guaranteed()
538 }
539
540 pub fn spend(&self) -> Point {
542 self.spend
543 }
544
545 pub fn view(&self) -> Point {
547 self.view
548 }
549}
550
551pub type MoneroAddress = Address<{ MONERO_BYTES.to_const_generic() }>;