1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
use self::super::{grammar, ParseError};
use crc::crc32::{self, Hasher32};
use std::collections::BTreeMap;
use std::{str, fmt, mem};
use std::io::Write;


/// [OpenAlias](https://openalias.org)-parsed cryptocurrency address.
///
/// `Display`ing an address with a checksum will *not* print out the same sum, but will re-hash the output string
/// (since the output can, while functionally equivalent, be different).
///
/// # Examples
///
/// Parse a simple example entry:
///
/// ```
/// # use openalias::CryptoAddress;
/// # use std::collections::BTreeMap;
/// static MONERO_DONATE_RCRD: &str =
///    "oa1:xmr \
///     recipient_address=46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcu\
///                       wufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em; \
///     recipient_name=Monero Development;";
/// assert_eq!(MONERO_DONATE_RCRD.parse::<CryptoAddress>().unwrap(),
///         CryptoAddress {
///             cryptocurrency: "xmr".to_string(),
///             address: "46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcu\
///                       wufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em".to_string(),
///
///             recipient_name: Some("Monero Development".to_string()),
///             tx_description: None,
///             tx_amount: None,
///             tx_payment_id: None,
///             address_signature: None,
///             checksum: None,
///
///             additional_values: BTreeMap::new(),
///         });
/// ```
///
/// Parse a more complex record:
///
/// ```
/// # use openalias::CryptoAddress;
/// # use std::collections::BTreeMap;
/// static NAB_DONATE_RCRD: &str =
///     "oa1:btc recipient_address=1MoSyGZp3SKpoiXPXfZDFK7cDUFCVtEDeS; \
///      recipient_name=\"nabijaczleweli; FOSS development\";\
///      tx_description=Donation for nabijaczleweli:\\ ; \
///      tx_amount=0.1;checksum=D851342C; kaschism=yass;";
/// assert_eq!(NAB_DONATE_RCRD.parse::<CryptoAddress>().unwrap(),
///         CryptoAddress {
///             cryptocurrency: "btc".to_string(),
///             address: "1MoSyGZp3SKpoiXPXfZDFK7cDUFCVtEDeS".to_string(),
///
///             recipient_name: Some("nabijaczleweli; FOSS development".to_string()),
///             tx_description: Some("Donation for nabijaczleweli: ".to_string()),
///             tx_amount: Some("0.1".to_string()),
///             tx_payment_id: None,
///             address_signature: None,
///             checksum: Some((0xD851342C, true)),
///
///             additional_values: {
///                 let mut avs = BTreeMap::new();
///                 avs.insert("kaschism".to_string(), "yass".to_string());
///                 avs
///             },
///         });
/// ```
///
/// `Display` a record:
///
/// ```
/// # use openalias::CryptoAddress;
/// # use std::collections::BTreeMap;
/// let mut base_record = CryptoAddress {
///     cryptocurrency: "btc".to_string(),
///     address: "1MoSyGZp3SKpoiXPXfZDFK7cDUFCVtEDeS".to_string(),
///
///     recipient_name: Some("nabijaczleweli; FOSS development".to_string()),
///     tx_description: Some("Donation for nabijaczleweli: ".to_string()),
///     tx_amount: Some("0.1".to_string()),
///     tx_payment_id: None,
///     address_signature: None,
///     checksum: Some((0xD851342C, true)),
///
///     additional_values: {
///         let mut avs = BTreeMap::new();
///         avs.insert("kaschism".to_string(), "yass".to_string());
///         avs
///     },
/// };
///
/// assert_eq!(&base_record.to_string(),
///            "oa1:btc recipient_address=1MoSyGZp3SKpoiXPXfZDFK7cDUFCVtEDeS; \
///             recipient_name=\"nabijaczleweli; FOSS development\"; \
///             tx_description=Donation for nabijaczleweli:\\ ; tx_amount=0.1; \
///             kaschism=yass; checksum=5AAC58F4;");
///
/// base_record.checksum = None;
/// assert_eq!(&base_record.to_string(),
///            "oa1:btc recipient_address=1MoSyGZp3SKpoiXPXfZDFK7cDUFCVtEDeS; \
///             recipient_name=\"nabijaczleweli; FOSS development\"; \
///             tx_description=Donation for nabijaczleweli:\\ ; tx_amount=0.1; \
///             kaschism=yass;");
///
/// base_record.recipient_name = None;
/// base_record.tx_description = None;
/// base_record.tx_amount = None;
/// base_record.additional_values.clear();
/// assert_eq!(&base_record.to_string(),
///            "oa1:btc recipient_address=1MoSyGZp3SKpoiXPXfZDFK7cDUFCVtEDeS;");
/// ```
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct CryptoAddress {
    /// Specified cryptocurrency's name.
    ///
    /// Usually "btc" for Bitcoin, "mxr" for Monero, et caetera.
    ///
    /// Note, that:
    ///
    /// > OpenAlias does not maintain a repository of prefixes at this stage, but may do so in future.
    pub cryptocurrency: String,
    /// Recipient's specified cryptocurrency address. Required.
    ///
    /// Corresponds to `recipient_address` record key.
    pub address: String,

    /// Recipient's specified user-friendlier name.
    ///
    /// Corresponds to `recipient_name` record key.
    pub recipient_name: Option<String>,
    /// Description for the transaction(s) resulting from this record.
    ///
    /// Note, that:
    ///
    /// > Bear in mind that DNS is typically long-lived data and not always updated at request time, so this should only be
    /// used if it does not need to be updated constantly.
    ///
    /// Corresponds to `tx_description` record key.
    pub tx_description: Option<String>,
    /// Amount of the specified cryptocurrency for the transaction(s) resulting from this record.
    ///
    /// Exact numeric value/type is usecase-dependent. No restrictions are applied within the realm of the library.
    ///
    /// Corresponds to `tx_amount` record key.
    pub tx_amount: Option<String>,
    /// "Particular to Monero, but is standardised as other cryptocurrencies (CryptoNote-based cryptocurrencies in particular)
    /// may find it useful."
    ///
    /// > It is typically a hex string of 32 characters, but that is not enforced in the standard.
    ///
    /// Corresponds to `tx_payment_id` record key.
    pub tx_payment_id: Option<String>,
    /// "If you have a standardised way of signing messages based on the address private key, then this can be used to validate
    /// the FQDN."
    ///
    /// > The message that is signed should be the entire FQDN (eg. donate.getmonero.org) with nothing else.
    /// Validation would be to verify that the signature is valid for the FQDN as a message.
    ///
    /// Corresponds to `address_signature` record key.
    pub address_signature: Option<String>,
    /// CRC-32 of the record up to this key.
    ///
    /// Second value of the pair is whether the checksum verified correctly, provided for convenience.
    ///
    /// > Depending on your use-case, it may serve little or no purpose, although some may choose to include it for additional
    /// validation. In order to calculate or verify the checksum, take the entire record up until the checksum key-value pair
    /// (ie. excluding the checksum key-value pair). Strip any spaces from either side, and calculate the CRC-32 on that final
    /// record.
    ///
    /// Corresponds to `checksum` record key.
    pub checksum: Option<(u32, bool)>,

    /// Set of K-Vs not special-cased above.
    pub additional_values: BTreeMap<String, String>,
}

impl str::FromStr for CryptoAddress {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<CryptoAddress, ParseError> {
        let mut out = grammar::oa1(s)?;

        if let Some(ref mut checksum) = out.checksum.as_mut() {
            let before_checksum = s[0..s.find("checksum").unwrap()].trim();
            let mut dgst = crc32::Digest::new(crc32::IEEE);
            dgst.write(before_checksum.as_bytes());
            checksum.1 = dgst.sum32() == checksum.0;
        }

        Ok(out)
    }
}

impl fmt::Display for CryptoAddress {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fn escape_val(out: &mut Vec<u8>, val: &str) -> fmt::Result {
            if val.contains(';') {
                out.push(b'"');
                write!(out, "{}", val).map_err(|_| fmt::Error)?;
                out.push(b'"');
                out.push(b';');
            } else if val.starts_with(' ') || val.ends_with(' ') {
                for i in 0..val.find(|c| c != ' ').unwrap_or(0) {
                    out.push(b'\\');
                    out.push(b' ');
                    println!("before {}", i);
                }
                write!(out, "{}", val.trim()).map_err(|_| fmt::Error)?;
                for i in val.rfind(|c| c != ' ').unwrap_or_else(|| val.len() - 1)..val.len() - 1 {
                    out.push(b'\\');
                    out.push(b' ');
                    println!("after {}/{}", i, val.len());
                }
                out.push(b';');
            } else {
                write!(out, "{};", val).map_err(|_| fmt::Error)?;
            }
            Ok(())
        }


        let mut out = vec![];

        write!(out, "oa1:{} recipient_address=", self.cryptocurrency).map_err(|_| fmt::Error)?;
        escape_val(&mut out, &self.address)?;

        if let Some(recipient_name) = self.recipient_name.as_ref() {
            write!(out, " recipient_name=").map_err(|_| fmt::Error)?;
            escape_val(&mut out, recipient_name)?;
        }

        if let Some(tx_description) = self.tx_description.as_ref() {
            write!(out, " tx_description=").map_err(|_| fmt::Error)?;
            escape_val(&mut out, tx_description)?;
        }

        if let Some(tx_amount) = self.tx_amount.as_ref() {
            write!(out, " tx_amount=").map_err(|_| fmt::Error)?;
            escape_val(&mut out, tx_amount)?;
        }

        if let Some(tx_payment_id) = self.tx_payment_id.as_ref() {
            write!(out, " tx_payment_id=").map_err(|_| fmt::Error)?;
            escape_val(&mut out, tx_payment_id)?;
        }

        if let Some(address_signature) = self.address_signature.as_ref() {
            write!(out, " address_signature=").map_err(|_| fmt::Error)?;
            escape_val(&mut out, address_signature)?;
        }

        for (key, val) in &self.additional_values {
            write!(out, " {}=", key).map_err(|_| fmt::Error)?;
            escape_val(&mut out, val)?;
        }

        f.write_str(str::from_utf8(&out).map_err(|_| fmt::Error)?)?;

        if self.checksum.is_some() {
            let mut dgst = crc32::Digest::new(crc32::IEEE);
            dgst.write(&out);
            write!(f, " checksum={:01$X};", dgst.sum32(), mem::size_of::<u32>() * 2).map_err(|_| fmt::Error)?;
        }

        Ok(())
    }
}