summaryrefslogtreecommitdiff
path: root/btc-wire/src/info.rs
blob: cc61b04301e33783254d5e13de51bd4635e6287d (plain)
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
use bitcoin::{hashes::Hash, Txid};
use url::Url;

#[derive(Debug, Clone, Copy, thiserror::Error)]
pub enum DecodeErr {
    #[error("Unknown first byte: {0}")]
    UnknownFirstByte(u8),
    #[error(transparent)]
    UriPack(#[from] uri_pack::DecodeErr),
    #[error(transparent)]
    Hash(#[from] bitcoin::hashes::Error),
    #[error("Unexpected end of file")]
    UnexpectedEOF,
}

/// Encoded metadata for outgoing transaction
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Info {
    Transaction { wtid: [u8; 32], url: Url },
    Bounce { bounced: Txid },
}

// We leave a potential special meaning for u8::MAX
const BOUNCE_BYTE: u8 = u8::MAX - 1;

pub fn encode_info(info: &Info) -> Vec<u8> {
    let mut buffer = Vec::new();
    match info {
        Info::Transaction { wtid, url } => {
            buffer.push(if url.scheme() == "http" { 1 } else { 0 });
            buffer.extend_from_slice(wtid);
            let parts = format!("{}{}", url.domain().unwrap_or(""), url.path());
            let packed = uri_pack::pack_uri(&parts).unwrap();
            buffer.extend_from_slice(&packed);
            return buffer;
        }
        Info::Bounce { bounced: id } => {
            buffer.push(BOUNCE_BYTE);
            buffer.extend_from_slice(id.as_ref());
        }
    }
    return buffer;
}

pub fn decode_info(bytes: &[u8]) -> Result<Info, DecodeErr> {
    if bytes.is_empty() {
        return Err(DecodeErr::UnexpectedEOF);
    }
    match bytes[0] {
        0..=1 => {
            if bytes.len() < 33 {
                return Err(DecodeErr::UnexpectedEOF);
            }
            let packed = format!(
                "http{}://{}",
                if bytes[0] == 0 { "s" } else { "" },
                uri_pack::unpack_uri(&bytes[33..])?,
            );
            let url = Url::parse(&packed).unwrap();
            Ok(Info::Transaction {
                wtid: bytes[1..33].try_into().unwrap(),
                url,
            })
        }
        BOUNCE_BYTE => Ok(Info::Bounce {
            bounced: Txid::from_slice(&bytes[1..])?,
        }),
        unknown => Err(DecodeErr::UnknownFirstByte(unknown)),
    }
}

#[cfg(test)]
mod test {
    use bitcoin::{hashes::Hash, Txid};
    use btc_wire::test::rand_key;
    use url::Url;

    use crate::info::{decode_info, encode_info, Info};

    #[test]
    fn decode_encode_tx() {
        let urls = [
            "https://git.taler.net/",
            "https://git.taler.net/depolymerization.git/",
            "http://git.taler.net/",
            "http://git.taler.net/depolymerization.git/",
        ];
        for url in urls {
            let wtid = rand_key();
            let url = Url::parse(url).unwrap();
            let info = Info::Transaction { wtid, url };
            let encode = encode_info(&info);
            let decoded = decode_info(&encode).unwrap();
            assert_eq!(decoded, info);
        }
    }

    #[test]
    fn decode_encode_bounce() {
        for _ in 0..4 {
            let id = rand_key();
            let info = Info::Bounce {
                bounced: Txid::from_slice(&id).unwrap(),
            };
            let encode = encode_info(&info);
            let decoded = decode_info(&encode).unwrap();
            assert_eq!(decoded, info);
        }
    }
}