xrpl_address_macro/
lib.rs

1//! The `r_address!` macro for compile-time address conversion converts XRPL classic addresses (r-addresses)
2//! to 20-byte arrays at compile time.
3//!
4//! **Important**: The macro only accepts string literals, not runtime values.
5//! It runs during compilation and outputs only the final byte array - no base58
6//! decoding code is included in the WASM binary.
7//!
8//! # Example
9//! ```shell
10//! use xrpl_wasm_stdlib::r_address;
11//!
12//! // ✅ Works - compile-time literal
13//! const ACCOUNT: [u8; 20] = r_address!("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh");
14//!
15//! // ❌ Does NOT work - runtime value
16//! // fn convert(addr: &str) -> [u8; 20] {
17//! //     r_address!(addr)  // ERROR: expected string literal
18//! // }
19//! ```
20
21use proc_macro::TokenStream;
22use quote::quote;
23use syn::{LitStr, parse_macro_input};
24
25#[proc_macro]
26pub fn r_address(input: TokenStream) -> TokenStream {
27    let addr_lit = parse_macro_input!(input as LitStr);
28    let addr = addr_lit.value();
29
30    match decode_classic_address_to_20bytes(&addr) {
31        Some(bytes) => {
32            if bytes.len() != 20 {
33                return syn::Error::new(
34                    addr_lit.span(),
35                    format!("Address decoded to {} bytes, expected 20", bytes.len()),
36                )
37                .to_compile_error()
38                .into();
39            }
40
41            let bytes_tokens = bytes.iter().map(|b| quote! { #b });
42            let expanded = quote! {
43                [#(#bytes_tokens),*]
44            };
45
46            TokenStream::from(expanded)
47        }
48        None => syn::Error::new(addr_lit.span(), format!("Invalid r-address: {addr}"))
49            .to_compile_error()
50            .into(),
51    }
52}
53
54fn decode_classic_address_to_20bytes(addr: &str) -> Option<Vec<u8>> {
55    if !addr.starts_with('r') {
56        return None;
57    }
58    let alphabet =
59        bs58::Alphabet::new(b"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz").ok()?;
60    let full = bs58::decode(addr)
61        .with_alphabet(&alphabet)
62        .into_vec()
63        .ok()?;
64    if full.len() < 1 + 20 + 4 {
65        return None;
66    }
67    // Version byte should be 0x00 for classic AccountID
68    if full[0] != 0x00 {
69        return None;
70    }
71    // Split payload and checksum
72    let (payload, checksum) = full.split_at(full.len() - 4);
73    // Verify checksum: double SHA-256 of payload, take first 4 bytes
74    use sha2::{Digest, Sha256};
75    let first = Sha256::digest(payload);
76    let second = Sha256::digest(first);
77    if &second[0..4] != checksum {
78        return None;
79    }
80    // Payload is version (1) + 20 bytes account id
81    if payload.len() != 1 + 20 {
82        return None;
83    }
84    Some(payload[1..].to_vec())
85}