depolymerization

wire gateway for Bitcoin/Ethereum
Log | Files | Refs | Submodules | README | LICENSE

commit 8b4288f34ff7dcbde255cb417445bb09b58a5c33
parent 3826f041a9b3ca885d0ca8910d1f65ccd8f1c910
Author: Antoine A <>
Date:   Wed,  1 Dec 2021 17:25:30 +0100

Add packed reserve_base_url support and fix amount conversion

Diffstat:
MCargo.lock | 13+++++++------
Mbtc-wire/Cargo.toml | 2+-
Muri-pack/Cargo.toml | 11+++++++++--
Auri-pack/benches/pack.rs | 36++++++++++++++++++++++++++++++++++++
Auri-pack/src/lib.rs | 324+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Muri-pack/src/main.rs | 251+------------------------------------------------------------------------------
Mwire-gateway/Cargo.toml | 2++
Mwire-gateway/src/main.rs | 227++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
8 files changed, 525 insertions(+), 341 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -208,9 +208,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "2.33.3" +version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "bitflags", "textwrap", @@ -230,9 +230,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3825b1e8580894917dc4468cb634a1b4e9745fddc854edad72d9c04644c0319f" +checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" dependencies = [ "cfg-if", ] @@ -1324,9 +1324,9 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" name = "uri-pack" version = "0.1.0" dependencies = [ + "criterion", "csv", - "idna", - "percent-encoding", + "fastrand", "serde_json", "thiserror", "url", @@ -1531,5 +1531,6 @@ dependencies = [ "serde_with", "thiserror", "tokio", + "uri-pack", "url", ] diff --git a/btc-wire/Cargo.toml b/btc-wire/Cargo.toml @@ -14,7 +14,7 @@ rustyline = "9.0.0" bech32 = "0.8.1" # Secure random rand = { version = "0.8.4", features = ["getrandom"] } -# Fast unsecure random +# Fast insecure random fastrand = "1.5.0" # Serialization library serde = { version = "1.0.130", features = ["derive"] } diff --git a/uri-pack/Cargo.toml b/uri-pack/Cargo.toml @@ -15,5 +15,12 @@ csv = "1.1.6" serde_json = "1.0.72" # Url parser url = "2.2.2" -idna = "0.2.3" -percent-encoding = "2.1.0" +# statistics-driven micro-benchmarks +criterion = "0.3.5" +# Fast insecure random +fastrand = "1.5.0" + +[[bench]] +name = "pack" +harness = false + diff --git a/uri-pack/benches/pack.rs b/uri-pack/benches/pack.rs @@ -0,0 +1,36 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use uri_pack::{pack_uri, unpack_uri}; + +fn rand_compat(size: usize) -> String { + String::from_utf8( + std::iter::repeat_with(|| fastrand::u8(b' '..=b'~')) + .take(size) + .collect(), + ) + .unwrap() +} + +fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("Uri"); + for size in [50, 500, 4048].iter() { + group.throughput(Throughput::Bytes(*size as u64)); + group.bench_with_input(BenchmarkId::new("pack", size), size, |b, &size| { + b.iter_batched( + || rand_compat(size), + |uri| pack_uri(&uri).unwrap(), + criterion::BatchSize::SmallInput, + ) + }); + group.bench_with_input(BenchmarkId::new("unpack", size), size, |b, &size| { + b.iter_batched( + || pack_uri(&rand_compat(size)).unwrap(), + |(packed, len)| unpack_uri(&packed, len), + criterion::BatchSize::SmallInput, + ) + }); + } + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/uri-pack/src/lib.rs b/uri-pack/src/lib.rs @@ -0,0 +1,324 @@ +/// Reserved 5 bit code for extended packing +const EXTENDED: u8 = 31; + +/// Packed ascii as 5 or 11 bits +#[derive(Debug, Clone, Copy)] +pub enum Packed { + Simple(u8), // u5: 0..32 + Extended(u8), // u6: 0..54 +} + +/// Pack an URI ascii char +/// Panic if char not supported +fn pack_ascii(c: u8) -> Packed { + let simple = match c { + b'a'..=b'z' => c - b'a', + b'.' => 26, + b'/' => 27, + b'-' => 28, + b'_' => 29, + b'%' => 30, + c => { + let extended = match c { + b'A'..=b'Z' => c - b'A', + b'0'..=b'9' => c - b'0' + 26, + b'!' => 36, + b'"' => 37, + b'#' => 38, + b'$' => 39, + b'&' => 40, + b'\'' => 41, + b'(' => 42, + b')' => 43, + b'*' => 44, + b'+' => 45, + b',' => 46, + b':' => 47, + b';' => 48, + b'<' => 49, + b'=' => 50, + b'>' => 51, + b'?' => 52, + b'@' => 53, + b'[' => 54, + b'\\' => 55, + b']' => 56, + b'^' => 57, + b'`' => 58, + b'{' => 59, + b'|' => 60, + b'}' => 61, + b'~' => 62, + b' ' => 63, + _ => unreachable!(), + }; + return Packed::Extended(extended); + } + }; + Packed::Simple(simple) +} + +/// Unpack an URI ascii char +/// Panic if char not supported +fn unpack_ascii(c: Packed) -> u8 { + match c { + Packed::Simple(c) => match c { + 0..=25 => b'a' + c, + 26 => b'.', + 27 => b'/', + 28 => b'-', + 29 => b'_', + 30 => b'%', + _ => unreachable!(), + }, + Packed::Extended(c) => match c { + 0..=25 => b'A' + c, + 26..=35 => b'0' + c - 26, + 36 => b'!', + 37 => b'"', + 38 => b'#', + 39 => b'$', + 40 => b'&', + 41 => b'\'', + 42 => b'(', + 43 => b')', + 44 => b'*', + 45 => b'+', + 46 => b',', + 47 => b':', + 48 => b';', + 49 => b'<', + 50 => b'=', + 51 => b'>', + 52 => b'?', + 53 => b'@', + 54 => b'[', + 55 => b'\\', + 56 => b']', + 57 => b'^', + 58 => b'`', + 59 => b'{', + 60 => b'|', + 61 => b'}', + 62 => b'~', + 63 => b' ', + _ => unreachable!(), + }, + } +} +/// Check if an ascii char is packable +fn supported_ascii(c: &u8) -> bool { + (b' '..=b'~').contains(c) +} + +#[derive(Debug, Clone, Copy, thiserror::Error)] +pub enum EncodeErr { + #[error("{0} is not a valid uri char")] + UnsupportedChar(u8), +} + +#[derive(Debug, Clone, Copy, thiserror::Error)] +pub enum DecodeErr { + #[error("An extended encoded char have been passed as an simple one")] + ExpectedExtended, + #[error("{0} is not an simple encoded char")] + UnexpectedSimpleChar(u8), + #[error("{0} is not an extended encoded char")] + UnexpectedExtendedChar(u8), + #[error("Missing bits")] + UnexpectedEOF, +} + +/// Pack an uri string into an optimized binary format +pub fn pack_uri(uri: &str) -> Result<(Vec<u8>, usize), EncodeErr> { + let len = uri.as_bytes().len(); + let mut vec = Vec::with_capacity(len); + + if let Some(char) = uri.as_bytes().iter().find(|c| !supported_ascii(c)) { + return Err(EncodeErr::UnsupportedChar(*char)); + } + + // Holds pending bits beginning from the most significant bits + // If buff contains 10110: + // buff_bits ───── + // buff 10110000 + let (mut buff, mut buff_bits) = (0u8, 0u8); + + let mut write_bits = |nb: u8, mut nb_bits: u8| { + // buff_bits ───── + // buff 10110000 + // nb_bits ──── + // nb 00011010 + // + // ─── + // writable 00011010 + // ─── + // rmv_right 00001101 + // ─── + // rmv_left 10100000 + // ─── + // align 00000101 + // + // buff_bits ──────── + // buff 10110101 + // nb_bits - + // nb 00011010 + while nb_bits > 0 { + // Amount of bits we can write in buff + let writable = (8 - buff_bits).min(nb_bits); + // Remove non writable bits + let rmv_right = nb >> nb_bits - writable; + let rmv_left = rmv_right << 8 - writable; + // Align remaining bits with buff blank bits + let align = rmv_left >> buff_bits; + + // Write bits in buff + buff = buff | align; + buff_bits += writable; + nb_bits -= writable; + + // Store buff if full + if buff_bits == 8 { + vec.push(buff); + buff = 0; + buff_bits = 0; + } + } + }; + + for c in uri.bytes() { + match pack_ascii(c) { + Packed::Simple(nb) => write_bits(nb, 5), + Packed::Extended(nb) => { + write_bits(EXTENDED, 5); + write_bits(nb, 6); + } + } + } + + // Push pending buffer if not empty + if buff_bits > 0 { + vec.push(buff); + } + + return Ok((vec, len)); +} + +/// Unpack an uri string from its optimized binary format +pub fn unpack_uri(bytes: &[u8], len: usize) -> Result<String, DecodeErr> { + let mut buf = String::with_capacity(len); + let mut iter = bytes.iter(); + + // Holds pending bits beginning from the most significant bits + // If buff contains 10110: + // buff_bits ───── + // buff 10110000 + let (mut buff, mut buff_bits) = (0u8, 0u8); + + let mut read_nb = |mut nb_bits: u8| -> Result<u8, DecodeErr> { + // buff_bits ───── + // buff 10110000 + // nb_bits ─── + // nb 00000000 + // + // ─── + // readable 10110000 + // ─── + // rmv_left 01100000 + // ─── + // align 00000011 + // + // buff_bits ─ + // buff 10110000 + // nb_bits + // nb 00000011 + + let mut nb = 8; + while nb_bits > 0 { + // Load buff if empty + if buff_bits == 0 { + buff = *iter.next().ok_or(DecodeErr::UnexpectedEOF)?; + buff_bits = 8; + } + // Amount of bits we can read from buff + let readable = buff_bits.min(nb_bits); + // Remove non writable bits + let rmv_left = buff << 8 - buff_bits; + // Align remaining bits with nb blank bits + let align = rmv_left >> (8 - readable); + // Read bits from buff + nb = (nb << readable) | align; + buff_bits -= readable; + nb_bits -= readable; + } + return Ok(nb); + }; + + for _ in 0..len { + let encoded = match read_nb(5)? { + EXTENDED => Packed::Extended(read_nb(6)?), + nb => Packed::Simple(nb), + }; + buf.push(unpack_ascii(encoded) as char); + } + + return Ok(buf); +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use serde_json::Value; + + use crate::{pack_ascii, pack_uri, supported_ascii, unpack_ascii, unpack_uri}; + + #[test] + /// Check support every ascii graphic character and space + fn supported() { + for c in (0..=255u8).filter(supported_ascii) { + assert_eq!(unpack_ascii(pack_ascii(c)), c); + } + } + + #[test] + /// Check error on unsupported char + fn unsupported() { + for c in (0..=255u8).filter(|c| !supported_ascii(c)) { + let string = String::from(c as char); + assert!(pack_uri(&string).is_err()); + } + } + + #[test] + fn url_simple() { + let mut majestic = + csv::Reader::from_reader(include_str!("majestic_million.csv").as_bytes()); + for record in majestic.records() { + let domain = &record.unwrap()[2]; + let (encoded, len) = pack_uri(domain).unwrap(); + let decoded = unpack_uri(&encoded, len).unwrap(); + assert_eq!(domain, decoded); + } + } + + #[test] + fn url_complex() { + let mut json = Value::from_str(include_str!("urltestdata.json")) + .expect("JSON parse error in urltestdata.json"); + for entry in json.as_array_mut().unwrap() { + if entry.is_string() { + continue; // ignore comments + } + + let href = entry.get("href").and_then(|it| it.as_str()).unwrap_or(""); + if href.chars().any(|c| !c.is_ascii_graphic() || c != ' ') { + continue; // extended ascii + } + let (encoded, len) = pack_uri(&href).expect(&format!("Failed to encode {}", &href)); + let decoded = + unpack_uri(&encoded, len).expect(&format!("Failed to decode encoded {}", &href)); + assert_eq!(href, decoded); + } + } +} diff --git a/uri-pack/src/main.rs b/uri-pack/src/main.rs @@ -1,3 +1,5 @@ +use uri_pack::pack_uri; + fn main() { let mut majestic = csv::Reader::from_reader(include_str!("majestic_million.csv").as_bytes()); let mut ascii_counter = [0u64; 255]; @@ -11,7 +13,7 @@ fn main() { } count += 1; before += domain.as_bytes().len(); - after += encode_str(domain).unwrap().len(); + after += pack_uri(domain).unwrap().0.len(); } let sum: u64 = ascii_counter.iter().sum(); for (ascii, count) in ascii_counter @@ -31,250 +33,3 @@ fn main() { } println!("\nBefore: {} After: {}", before / count, after / count); } - -#[derive(Debug, Clone, Copy)] -pub enum Encoded { - Simple(u8), // u5: 0..32 - Extended(u8), // u6: 0..54 -} - -pub fn encode_ascii(c: u8) -> Encoded { - let simple = match c { - b'a'..=b'z' => c - b'a', - b'.' => 26, - b'/' => 27, - b'-' => 28, - b'_' => 29, - b'%' => 30, - c => { - let extended = match c { - b'A'..=b'Z' => c as u8 - 'A' as u8, - b'0'..=b'9' => c as u8 - '0' as u8 + 26, - b'!' => 36, - b'"' => 37, - b'#' => 38, - b'$' => 39, - b'&' => 40, - b'\'' => 41, - b'(' => 42, - b')' => 43, - b'*' => 44, - b'+' => 45, - b',' => 46, - b':' => 47, - b';' => 48, - b'<' => 49, - b'=' => 50, - b'>' => 51, - b'?' => 52, - b'@' => 53, - b'[' => 54, - b'\\' => 55, - b']' => 56, - b'^' => 57, - b'`' => 58, - b'{' => 59, - b'|' => 60, - b'}' => 61, - b'~' => 62, - b' ' => 63, - _ => unreachable!(), - }; - return Encoded::Extended(extended); - } - }; - Encoded::Simple(simple) -} - -pub fn decode_ascii(c: Encoded) -> u8 { - match c { - Encoded::Simple(c) => match c { - 0..=25 => b'a' + c, - 26 => b'.', - 27 => b'/', - 28 => b'-', - 29 => b'_', - 30 => b'%', - _ => unreachable!(), - }, - Encoded::Extended(c) => match c { - 0..=25 => b'A' + c, - 26..=35 => b'0' + c - 26, - 36 => b'!', - 37 => b'"', - 38 => b'#', - 39 => b'$', - 40 => b'&', - 41 => b'\'', - 42 => b'(', - 43 => b')', - 44 => b'*', - 45 => b'+', - 46 => b',', - 47 => b':', - 48 => b';', - 49 => b'<', - 50 => b'=', - 51 => b'>', - 52 => b'?', - 53 => b'@', - 54 => b'[', - 55 => b'\\', - 56 => b']', - 57 => b'^', - 58 => b'`', - 59 => b'{', - 60 => b'|', - 61 => b'}', - 62 => b'~', - 63 => b' ', - _ => unreachable!(), - }, - } -} - -const EXTENDED: u8 = 31; - -#[derive(Debug, Clone, Copy, thiserror::Error)] -#[error("{0} is not a valid uri char")] -pub struct EncodeErr(char); - -#[derive(Debug, Clone, Copy, thiserror::Error)] -pub enum DecodeErr { - #[error("An extended encoded char have been passed as an simple one")] - ExpectedExtended, - #[error("{0} is not an simple encoded char")] - UnexpectedSimpleChar(u8), - #[error("{0} is not an extended encoded char")] - UnexpectedExtendedChar(u8), -} - -pub fn encode_str(str: &str) -> Result<Vec<u8>, EncodeErr> { - let mut vec = Vec::new(); - - assert!(str.as_bytes().iter().all(|c| supported_char(*c as char))); - - // Amount of pending bits stored in buffer. - let mut buffer_bits = 0u8; - // Holds pending bits beginning from the most significant bits - let mut buffer: u8 = 0; - let mut write_bits = |nb: u8, mut nb_bits: u8| { - while nb_bits > 0 { - let writable = (8 - buffer_bits).min(nb_bits); - let remove_right = nb_bits - writable; - let remove_left = 8 - writable; - let mask = ((nb >> remove_right) << (remove_left)) >> buffer_bits; - buffer = buffer | mask; - buffer_bits += writable; - nb_bits -= writable; - // Write filled byte - if buffer_bits == 8 { - vec.push(buffer); - buffer = 0; - buffer_bits = 0; - } - } - }; - - for c in str.bytes() { - match encode_ascii(c) { - Encoded::Simple(nb) => write_bits(nb, 5), - Encoded::Extended(nb) => { - write_bits(EXTENDED, 5); - write_bits(nb, 6); - } - } - } - - if buffer_bits > 0 { - vec.push(buffer); - } - - return Ok(vec); -} - -pub fn decode_str(bytes: &[u8], len: usize) -> Result<String, DecodeErr> { - let mut buf = String::with_capacity(len); - let mut iter = bytes.iter(); - // Amount of pending bits stored in buffer. - let mut buffer_bits = 0u8; - // Holds pending bits beginning from the most significant bits - let mut buffer: u8 = 0; - let mut read_nb = |mut nb_bits: u8| -> u8 { - let mut nb = 8; - while nb_bits > 0 { - if buffer_bits == 0 { - buffer = *iter.next().unwrap(); - buffer_bits = 8; - } - let readable = buffer_bits.min(nb_bits); - let mask = (buffer << 8 - buffer_bits) >> (8 - readable); - nb = (nb << readable) | mask; - buffer_bits -= readable; - nb_bits -= readable; - } - return nb; - }; - - for _ in 0..len { - let encoded = match read_nb(5) { - EXTENDED => Encoded::Extended(read_nb(6)), - nb => Encoded::Simple(nb), - }; - buf.push(decode_ascii(encoded) as char); - } - - return Ok(buf); -} - -fn supported_char(c: char) -> bool { - c.is_ascii_graphic() || c == ' ' -} - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use serde_json::Value; - - use crate::{decode_ascii, decode_str, encode_ascii, encode_str, supported_char}; - - #[test] - /// Check support every ascii graphic character and space - fn supported() { - for c in (0..=255u8).filter(|c| supported_char(*c as char)) { - assert_eq!(decode_ascii(encode_ascii(c)), c); - } - } - - #[test] - fn url_simple() { - let mut majestic = - csv::Reader::from_reader(include_str!("majestic_million.csv").as_bytes()); - for record in majestic.records() { - let domain = &record.unwrap()[2]; - let decoded = decode_str(&encode_str(domain).unwrap(), domain.len()).unwrap(); - assert_eq!(domain, decoded); - } - } - - #[test] - fn url_complex() { - let mut json = Value::from_str(include_str!("urltestdata.json")) - .expect("JSON parse error in urltestdata.json"); - for entry in json.as_array_mut().unwrap() { - if entry.is_string() { - continue; // ignore comments - } - - let href = entry.get("href").and_then(|it| it.as_str()).unwrap_or(""); - if href.chars().any(|c| !c.is_ascii_graphic() || c != ' ') { - continue; // extended ascii - } - let encoded = encode_str(&href).expect(&format!("Failed to encode {}", &href)); - let decoded = decode_str(&encoded, href.len()) - .expect(&format!("Failed to decode encoded {}", &href)); - assert_eq!(href, decoded); - } - } -} diff --git a/wire-gateway/Cargo.toml b/wire-gateway/Cargo.toml @@ -32,3 +32,5 @@ rand = { version = "0.8.4", features = ["getrandom"] } url = { version = "2.2.2", features = ["serde"] } # Bitcoin taler util btc-wire = { path = "../btc-wire" } +# Optimized uri binary format +uri-pack = { path = "../uri-pack" } diff --git a/wire-gateway/src/main.rs b/wire-gateway/src/main.rs @@ -59,7 +59,7 @@ fn btc_payto_addr(url: &Url) -> Result<&str, ServerErr> { impl Into<Amount> for BtcAmount { fn into(self) -> Amount { let sat = self.as_sat(); - return Amount::new("BTC", sat / 100_000_000, ((sat % 100_000_000) / 10) as u32); + return Amount::new("BTC", sat / 100_000_000, (sat % 100_000_000) as u32); } } @@ -71,11 +71,60 @@ impl TryFrom<Amount> for BtcAmount { return Err("Wrong currency".to_string()); } - let sat = value.value * 100_000_000 + value.fraction as u64 * 10; + let sat = value.value * 100_000_000 + value.fraction as u64; return Ok(Self::from_sat(sat)); } } +fn encode_info(wtid: &[u8; 32], url: &Url) -> Vec<u8> { + let mut buffer = Vec::new(); + buffer.extend_from_slice(wtid); + let parts = format!("{}{}", url.domain().unwrap_or(""), url.path()); + let (packed, len) = uri_pack::pack_uri(&parts).unwrap(); + buffer.push((url.scheme() == "http:") as u8); + buffer.push(len as u8); + buffer.extend_from_slice(&packed); + return buffer; +} + +fn decode_info(bytes: &[u8]) -> ([u8; 32], Url) { + let len = bytes[33] as usize; + let mut packed = uri_pack::unpack_uri(&bytes[34..], len).unwrap(); + packed.insert_str(0, "://"); + if bytes[32] == 0 { + packed.insert(0, 's'); + } + packed.insert_str(0, "http"); + let url = Url::parse(&packed).unwrap(); + return (bytes[..32].try_into().unwrap(), url); +} + +#[cfg(test)] +mod test { + use btc_wire::test::rand_key; + use url::Url; + + use crate::{decode_info, encode_info}; + + #[test] + fn decode_encode_info() { + let key = rand_key(); + let urls = [ + "https://git.taler.net/", + "https://git.taler.net/depolymerization.git/", + ]; + + for url in urls { + let url = Url::parse(url).unwrap(); + let encode = encode_info(&key, &url); + let decode = decode_info(&encode); + assert_eq!(key, decode.0); + assert_eq!(url, decode.1); + dbg!(encode.len() - 32, urls.len()); + } + } +} + #[tokio::main] async fn main() { let network = dirty_guess_network(); @@ -93,91 +142,99 @@ async fn main() { // BTC worker thread std::thread::spawn(move || { - let rpc = wallet_rpc(network, "wire"); - let self_addr = rpc.get_new_address(None, None).unwrap(); - let mut last_hash: Option<BlockHash> = None; - let confirmation = 1; - - loop { - let txs = rpc - .list_since_block(last_hash.as_ref(), Some(confirmation), None, Some(true)) - .unwrap(); - last_hash = Some(txs.lastblock); - - // List all confirmed send and receive transactions since last check - let txs: HashMap<Txid, Category> = txs - .transactions - .into_iter() - .filter_map(|tx| { - let cat = tx.detail.category; - (tx.info.confirmations >= confirmation as i32 - && (cat == Category::Send || cat == Category::Receive)) - .then(|| (tx.info.txid, cat)) - }) - .collect(); + let result: Result<(), Box<dyn std::error::Error>> = (move || { + let rpc = wallet_rpc(network, "wire"); + let self_addr = rpc.get_new_address(None, None)?; + let mut last_hash: Option<BlockHash> = None; + let confirmation = 1; + + loop { + let txs = + rpc.list_since_block(last_hash.as_ref(), Some(confirmation), None, Some(true))?; + last_hash = Some(txs.lastblock); - for (id, category) in txs { - match category { - Category::Send => match rpc.get_tx_op_return(&id) { - Ok((full, wtid)) => { - let credit_addr = sender_address(&rpc, &full).unwrap(); - let time = full.tx.info.blocktime.unwrap(); - let date = - Timestamp::from(SystemTime::UNIX_EPOCH + Duration::from_secs(time)); - let amount = full.tx.amount.abs().to_unsigned().unwrap().into(); - let mut lock = state.outgoing.blocking_lock(); - println!("{} >> {} {}", &self_addr, &credit_addr, &amount); - let array: [u8; 32] = wtid[..32].try_into().unwrap(); - let wtid = Base32::from(array); - let row_id = lock.len() as u64 + 1; - lock.push(OutgoingTransaction { - row_id: SafeUint64::try_from(row_id).unwrap(), - date, - amount, - debit_account: btc_payto_url(&self_addr), - credit_account: btc_payto_url(&credit_addr), - wtid, - }); - } - Err(err) => match err { - GetOpReturnErr::MissingOpReturn => {} // ignore - err => println!("send: {} {}", id, err), + // List all confirmed send and receive transactions since last check + let txs: HashMap<Txid, Category> = txs + .transactions + .into_iter() + .filter_map(|tx| { + let cat = tx.detail.category; + (tx.info.confirmations >= confirmation as i32 + && (cat == Category::Send || cat == Category::Receive)) + .then(|| (tx.info.txid, cat)) + }) + .collect(); + + for (id, category) in txs { + match category { + Category::Send => match rpc.get_tx_op_return(&id) { + Ok((full, metadata)) => { + let (wtid, exchange_base_url) = decode_info(&metadata); + let credit_addr = sender_address(&rpc, &full)?; + let time = full.tx.info.blocktime.unwrap(); + let date = Timestamp::from( + SystemTime::UNIX_EPOCH + Duration::from_secs(time), + ); + let amount = full.tx.amount.abs().to_unsigned()?.into(); + let mut lock = state.outgoing.blocking_lock(); + println!("{} >> {} {}", &self_addr, &credit_addr, &amount); + let array: [u8; 32] = wtid[..32].try_into()?; + let wtid = Base32::from(array); + let row_id = lock.len() as u64 + 1; + lock.push(OutgoingTransaction { + row_id: SafeUint64::try_from(row_id)?, + date, + amount, + debit_account: btc_payto_url(&self_addr), + credit_account: btc_payto_url(&credit_addr), + wtid, + exchange_base_url, + }); + } + Err(err) => match err { + GetOpReturnErr::MissingOpReturn => {} // ignore + err => println!("send: {} {}", id, err), + }, }, - }, - Category::Receive => match rpc.get_tx_segwit_key(&id) { - Ok((full, reserve_pub)) => { - let debit_addr = sender_address(&rpc, &full).unwrap(); - let credit_addr = full.tx.details[0].address.as_ref().unwrap(); - let time = full.tx.info.blocktime.unwrap(); - let date = - Timestamp::from(SystemTime::UNIX_EPOCH + Duration::from_secs(time)); - let amount = full.tx.amount.to_unsigned().unwrap().into(); - let mut lock = state.incoming.blocking_lock(); - println!("{} << {} {}", &debit_addr, &credit_addr, &amount); - let row_id = lock.len() as u64 + 1; - lock.push(IncomingTransaction { - row_id: SafeUint64::try_from(row_id).unwrap(), - date, - amount, - reserve_pub: reserve_pub.into(), - debit_account: btc_payto_url(&debit_addr), - credit_account: btc_payto_url(credit_addr), - }); - } - Err(err) => match err { - GetSegwitErr::Decode( - DecodeSegWitErr::MissingSegWitAddress - | DecodeSegWitErr::NoMagicIdMatch, - ) => {} - err => println!("receive: {} {}", id, err), + Category::Receive => match rpc.get_tx_segwit_key(&id) { + Ok((full, reserve_pub)) => { + let debit_addr = sender_address(&rpc, &full)?; + let credit_addr = full.tx.details[0].address.as_ref().unwrap(); + let time = full.tx.info.blocktime.unwrap(); + let date = Timestamp::from( + SystemTime::UNIX_EPOCH + Duration::from_secs(time), + ); + let amount: Amount = full.tx.amount.to_unsigned().unwrap().into(); + dbg!(full.tx.amount.to_unsigned(), amount); + let amount = full.tx.amount.to_unsigned()?.into(); + let mut lock = state.incoming.blocking_lock(); + println!("{} << {} {}", &debit_addr, &credit_addr, &amount); + let row_id = lock.len() as u64 + 1; + lock.push(IncomingTransaction { + row_id: SafeUint64::try_from(row_id)?, + date, + amount, + reserve_pub: reserve_pub.into(), + debit_account: btc_payto_url(&debit_addr), + credit_account: btc_payto_url(credit_addr), + }); + } + Err(err) => match err { + GetSegwitErr::Decode( + DecodeSegWitErr::MissingSegWitAddress + | DecodeSegWitErr::NoMagicIdMatch, + ) => {} + err => println!("receive: {} {}", id, err), + }, }, - }, - Category::Generate | Category::Immature | Category::Orphan => {} + Category::Generate | Category::Immature | Category::Orphan => {} + } } + println!("Wait for block"); + rpc.wait_for_new_block(0).ok(); } - println!("Wait for block"); - rpc.wait_for_new_block(0).ok(); - } + })(); + dbg!(result).unwrap(); }); let addr = ([0, 0, 0, 0], 8080).into(); @@ -234,6 +291,7 @@ struct OutgoingTransaction { wtid: ShortHashCode, debit_account: Url, credit_account: Url, + exchange_base_url: Url, } struct ServerState { @@ -310,8 +368,9 @@ async fn router( ErrorCode::GENERIC_PARAMETER_MALFORMED, ) })?; + let metadata = encode_info(&request.wtid, &request.exchange_base_url); client - .send_op_return(&to, amount, request.wtid.as_ref()) + .send_op_return(&to, amount, &metadata) .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::INVALID))?; let timestamp = Timestamp::now(); json_response( @@ -361,7 +420,7 @@ async fn router( credit_account: tx.credit_account.clone(), wtid: tx.wtid.clone(), debit_account: tx.debit_account.clone(), - exchange_base_url: Url::parse("http://localhost:8080").unwrap(), + exchange_base_url: tx.exchange_base_url.clone(), }) .collect(); json_response(