ur: scanning and creating qr codes for slatepack messages

This commit is contained in:
ardocrat 2024-05-27 00:20:16 +03:00
parent 4b46e5a997
commit 3f03d145e8
10 changed files with 373 additions and 110 deletions

73
Cargo.lock generated
View file

@ -1059,6 +1059,21 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitcoin-private"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57"
[[package]]
name = "bitcoin_hashes"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501"
dependencies = [
"bitcoin-private",
]
[[package]]
name = "bitflags"
version = "0.9.1"
@ -1800,6 +1815,21 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.0"
@ -3705,6 +3735,7 @@ dependencies = [
"tor-keymgr",
"tor-llcrypto",
"tor-rtcompat",
"ur",
"url",
"wgpu",
"winit",
@ -5492,6 +5523,26 @@ dependencies = [
"unicase",
]
[[package]]
name = "minicbor"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7005aaf257a59ff4de471a9d5538ec868a21586534fff7f85dd97d4043a6139"
dependencies = [
"minicbor-derive",
]
[[package]]
name = "minicbor-derive"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267"
dependencies = [
"proc-macro2 1.0.81",
"quote 1.0.36",
"syn 1.0.109",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -7102,6 +7153,15 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "rand_xoshiro"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "range-alloc"
version = "0.1.3"
@ -10258,6 +10318,19 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ur"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "010f24a953db5d22d0010969ca3bbf40b3857b89f47c0f7be0da4c2d7ded0760"
dependencies = [
"bitcoin_hashes",
"crc",
"minicbor",
"phf",
"rand_xoshiro",
]
[[package]]
name = "url"
version = "2.5.0"

View file

@ -57,6 +57,7 @@ tokio = { version = "1.37.0", features = ["full"] }
image = "0.25.1"
rqrr = "0.7.1"
qrcodegen = "1.8.0"
ur = "0.4.1"
## tor
arti = { version = "1.2.0", features = ["pt-client", "static"] }

View file

@ -35,18 +35,26 @@ use crate::wallet::WalletUtils;
/// Camera QR code scanner.
pub struct CameraContent {
/// QR code scanning progress and result.
qr_scan_state: Arc<RwLock<QrScanState>>
qr_scan_state: Arc<RwLock<QrScanState>>,
/// Uniform Resources URIs collected from QR code scanning.
ur_data: Arc<RwLock<Option<Vec<String>>>>,
start: i64,
}
impl Default for CameraContent {
fn default() -> Self {
Self {
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
ur_data: Arc::new(RwLock::new(None)),
start: 0,
}
}
}
impl CameraContent {
/// Draw camera content.
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
// Draw last image from camera or loader.
if let Some(img_data) = cb.camera_image() {
@ -138,8 +146,13 @@ impl CameraContent {
r_scan.image_processing
}
/// Get UR scanning progress in percents.
fn ur_progress(&self) -> i32 {
0
}
/// Parse QR code from provided image data.
fn scan_qr(&self, data: &DynamicImage) {
fn scan_qr(&self, image_data: &DynamicImage) {
// Do not scan when another image is processing.
if self.image_processing() {
return;
@ -149,55 +162,117 @@ impl CameraContent {
let mut w_scan = self.qr_scan_state.write();
w_scan.image_processing = true;
}
// Launch scanner at separate thread.
let data = data.clone();
let image_data = image_data.clone();
let qr_scan_state = self.qr_scan_state.clone();
thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let ur_data = self.ur_data.clone();
let on_scan = async move {
// Prepare image data.
let img = data.to_luma8();
let img = image_data.to_luma8();
let mut img: rqrr::PreparedImage<image::GrayImage>
= rqrr::PreparedImage::prepare(img);
// Scan and save results.
let grids = img.detect_grids();
if let Some(g) = grids.get(0) {
let mut data = vec![];
if let Ok(_) = g.decode_to(&mut data) {
let mut qr_data = vec![];
if let Ok(_) = g.decode_to(&mut qr_data) {
// Setup scanned data into text.
let text = String::from_utf8(qr_data.clone()).unwrap_or("".to_string());
// Setup current text.
let cur_text = {
let r_scan = qr_scan_state.read();
let text = if let Some(res) = r_scan.qr_scan_result.clone() {
res.value()
res.text()
} else {
"".to_string()
};
text
};
let text = String::from_utf8(data.clone()).unwrap_or("".to_string());
if !data.is_empty() && (cur_text.is_empty() || text != cur_text) {
let result = Self::parse_scan_result(&data);
// Parse non-empty data if parsed text is different from saved.
if !qr_data.is_empty() && (cur_text.is_empty() || text != cur_text) {
let res = Self::parse_qr_code(qr_data);
match res {
QrScanResult::URPart(uri, index, total) => {
// Setup current UR data.
let mut cur_data = {
let r_data = ur_data.read();
let mut cur_data = vec!["".to_string(); total];
if let Some(d) = r_data.clone() {
cur_data = d;
}
cur_data
};
if !cur_data.contains(&uri) {
// Save part of UR data.
{
cur_data.insert(index, uri);
let mut w_data = ur_data.write();
*w_data = Some(cur_data.clone());
}
// Setup UR decoder.
let mut decoder = ur::Decoder::default();
for m in cur_data {
if !m.is_empty() {
if let Ok(_) = decoder.receive(m.as_str()) {
continue;
} else {
break;
}
}
}
// Check if UR data is complete.
if decoder.complete() {
if let Ok(data) = decoder.message() {
// Parse complete data.
let res = Self::parse_qr_code(data.unwrap_or(vec![]));
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(result);
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
_ => {
// Clean UR data.
let mut w_data = ur_data.write();
*w_data = None;
// Save scan result.
let mut w_scan = qr_scan_state.write();
w_scan.qr_scan_result = Some(res);
return;
}
}
}
}
}
// Reset scanning flag to process again.
{
let mut w_scan = qr_scan_state.write();
w_scan.image_processing = false;
}
});
};
// Launch scanner at separate thread.
thread::spawn(move || {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(on_scan);
});
}
fn parse_scan_result(data: &Vec<u8>) -> QrScanResult {
/// Parse QR code scan result.
fn parse_qr_code(data: Vec<u8>) -> QrScanResult {
// Check if string starts with Grin address prefix.
let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
let text = text_string.as_str();
println!("data: {}", text_string);
let text = text_string.trim();
if text.starts_with("tgrin") || text.starts_with("grin") {
if SlatepackAddress::try_from(text).is_ok() {
return QrScanResult::Address(ZeroingString::from(text));
@ -209,7 +284,25 @@ impl CameraContent {
return QrScanResult::Slatepack(ZeroingString::from(text));
}
// Check Compact SeedQR format (https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification).
// Check Uniform Resource data.
// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
if text.starts_with("ur:bytes/") {
let split = text.split("/").collect::<Vec<_>>();
if let Some(index_total) = split.get(1) {
if let Some((index, total)) = index_total.split_once("-") {
let index = index.parse::<usize>();
let total = total.parse::<usize>();
if index.is_ok() && total.is_ok() {
let index = index.unwrap() - 1;
let total = total.unwrap();
return QrScanResult::URPart(text_string, index, total);
}
}
}
}
// Check Compact SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#compactseedqr-specification
if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
// Setup words amount.
let total_bits = data.len() * 8;
@ -259,7 +352,8 @@ impl CameraContent {
}
}
// Check Standard SeedQR format (https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification).
// Check Standard SeedQR format.
// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md#standard-seedqr-specification
let only_numbers = || {
for c in text.chars() {
if !c.is_numeric() {
@ -316,7 +410,11 @@ impl CameraContent {
/// Reset camera content state to default.
pub fn clear_state(&mut self) {
// Clear QR code scanning state.
let mut w_scan = self.qr_scan_state.write();
*w_scan = QrScanState::default();
// Clear UR data.
let mut w_data = self.ur_data.write();
*w_data = None;
}
}

View file

@ -28,16 +28,27 @@ pub struct QrCodeContent {
/// Text to create QR code.
pub(crate) text: String,
/// Flag to draw animated QR with Uniform Resources
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
animated: bool,
/// Index of current image at animation.
animated_index: Option<usize>,
/// Time of last image draw.
animation_time: Option<i64>,
/// Texture handle to show image when created.
texture_handle: Option<TextureHandle>,
/// QR code image creation progress and result.
qr_creation_state: Arc<RwLock<QrCreationState>>
qr_creation_state: Arc<RwLock<QrCreationState>>,
}
impl QrCodeContent {
pub fn new(text: String) -> Self {
pub fn new(text: String, animated: bool) -> Self {
Self {
text,
animated,
animated_index: None,
animation_time: None,
texture_handle: None,
qr_creation_state: Arc::new(RwLock::new(QrCreationState::default())),
}
@ -45,7 +56,8 @@ impl QrCodeContent {
/// Draw QR code.
pub fn ui(&mut self, ui: &mut egui::Ui, text: String) {
// Get saved QR code image or load new one.
if self.animated {
// Create animated QR code image if not created.
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
@ -54,8 +66,61 @@ impl QrCodeContent {
ui.add_space(space);
});
// Create image from text if not loading.
self.create_image(text);
// Create multiple vector images from text if not creating.
if !self.creating() {
self.create_svg_list(text);
}
} else {
let svg_list = {
let r_create = self.qr_creation_state.read();
r_create.svg_list.clone().unwrap()
};
// Setup animated index.
let now = chrono::Utc::now().timestamp_millis();
if now - *self.animation_time.get_or_insert(now) > 100 {
if let Some(i) = self.animated_index {
self.animated_index = Some(i + 1);
}
if *self.animated_index.get_or_insert(0) == svg_list.len() {
self.animated_index = Some(0);
}
self.animation_time = Some(now);
}
let svg = svg_list[self.animated_index.unwrap_or(0)].clone();
// Create images from SVG data.
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
let color_img = load_svg_bytes_with_size(svg.as_slice(), Some(size)).unwrap();
// Create image texture.
let texture_handle = ui.ctx().load_texture("qr_code",
color_img.clone(),
TextureOptions::default());
self.texture_handle = Some(texture_handle.clone());
let img_size = egui::emath::vec2(color_img.width() as f32,
color_img.height() as f32);
let sized_img = SizedTexture::new(texture_handle.id(), img_size);
// Add image to content.
ui.add(egui::Image::from_texture(sized_img)
.max_height(ui.available_width())
.fit_to_original_size(1.0));
ui.ctx().request_repaint();
}
} else {
// Create vector QR code image if not created.
if !self.has_image() {
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
ui.vertical_centered(|ui| {
ui.add_space(space);
View::big_loading_spinner(ui);
ui.add_space(space);
});
// Create vector image from text if not creating.
if !self.creating() {
self.create_svg(text);
}
} else {
// Create image from SVG data.
let r_create = self.qr_creation_state.read();
@ -76,25 +141,46 @@ impl QrCodeContent {
.fit_to_original_size(1.0));
}
}
}
/// Check if image is creating.
/// Check if QR code is creating.
fn creating(&self) -> bool {
let r_create = self.qr_creation_state.read();
r_create.creating
}
/// Create multiple vector QR code images at separate thread.
fn create_svg_list(&self, text: String) {
let qr_creation_state = self.qr_creation_state.clone();
thread::spawn(move || {
let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap();
let mut data = Vec::with_capacity(encoder.fragment_count());
for _ in 0..encoder.fragment_count() {
let ur = encoder.next_part().unwrap();
if let Ok(qr) = QrCode::encode_text(ur.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
data.push(svg.into_bytes());
}
}
let mut w_create = qr_creation_state.write();
if !data.is_empty() {
w_create.svg_list = Some(data);
}
w_create.creating = false;
});
}
/// Check if image was created.
fn has_image(&self) -> bool {
let r_create = self.qr_creation_state.read();
r_create.svg.is_some()
r_create.svg.is_some() || r_create.svg_list.is_some()
}
/// Create QR code image at separate thread.
fn create_image(&self, text: String) {
/// Create vector QR code image at separate thread.
fn create_svg(&self, text: String) {
let qr_creation_state = self.qr_creation_state.clone();
if !self.creating() {
thread::spawn(move || {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Medium) {
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
let svg = Self::qr_to_svg(qr, 0);
let mut w_create = qr_creation_state.write();
w_create.creating = false;
@ -102,10 +188,9 @@ impl QrCodeContent {
}
});
}
}
/// Convert QR code to SVG string.
fn qr_to_svg(qr: qrcodegen::QrCode, border: i32) -> String {
fn qr_to_svg(qr: QrCode, border: i32) -> String {
let mut result = String::new();
let dimension = qr.size().checked_add(border.checked_mul(2).unwrap()).unwrap();
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";

View file

@ -154,19 +154,24 @@ pub enum QrScanResult {
Address(ZeroingString),
/// Parsed text.
Text(ZeroingString),
/// Parsed SeedQR https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md.
SeedQR(ZeroingString)
/// Recovery phrase in standard or compact SeedQR format.
/// https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md
SeedQR(ZeroingString),
/// Part of Uniform Resources as URI with current index and total messages amount.
/// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md
URPart(String, usize, usize),
}
impl QrScanResult {
/// Get scan result value.
pub fn value(&self) -> String {
/// Get text scanning result.
pub fn text(&self) -> String {
match self {
QrScanResult::Slatepack(text) => text,
QrScanResult::Address(text) => text,
QrScanResult::Text(text) => text,
QrScanResult::SeedQR(text) => text
}.to_string()
QrScanResult::Slatepack(text) => text.to_string(),
QrScanResult::Address(text) => text.to_string(),
QrScanResult::Text(text) => text.to_string(),
QrScanResult::SeedQR(text) => text.to_string(),
QrScanResult::URPart(uri, _, _) => uri.to_string(),
}
}
}
@ -191,8 +196,10 @@ impl Default for QrScanState {
pub struct QrCreationState {
// Flag to check if QR code image is creating.
pub creating: bool,
// Found QR code content.
pub svg: Option<Vec<u8>>
// Vector image data.
pub svg: Option<Vec<u8>>,
// Multiple vector image data.
pub svg_list: Option<Vec<Vec<u8>>>
}
impl Default for QrCreationState {
@ -200,6 +207,7 @@ impl Default for QrCreationState {
Self {
creating: false,
svg: None,
svg_list: None,
}
}
}

View file

@ -371,7 +371,7 @@ impl WalletContent {
cb: &dyn PlatformCallbacks) {
// Show scan result if exists or show camera content while scanning.
if let Some(result) = &self.qr_scan_result {
let mut result_text = result.value();
let mut result_text = result.text();
View::horizontal_line(ui, Colors::ITEM_STROKE);
ui.add_space(3.0);
ScrollArea::vertical()

View file

@ -173,13 +173,13 @@ impl WalletMessages {
request_edit: "".to_string(),
request_error: None,
request_qr: false,
request_qr_content: QrCodeContent::new("".to_string()),
request_qr_content: QrCodeContent::new("".to_string(), true),
request_loading: false,
request_result: Arc::new(RwLock::new(None)),
message_camera_content: CameraContent::default(),
message_scan_error: false,
qr_message_text: None,
qr_message_content: QrCodeContent::new("".to_string()),
qr_message_content: QrCodeContent::new("".to_string(), true),
}
}
@ -472,7 +472,7 @@ impl WalletMessages {
} else {
t!("wallets.send_request_desc","amount" => amount_format)
};
ui.label(RichText::new(desc_text).size(16.0).color(Colors::INACTIVE_TEXT));
ui.label(RichText::new(desc_text).size(16.0).color(Colors::GRAY));
});
ui.add_space(6.0);

View file

@ -145,7 +145,7 @@ impl WalletTransport {
show_address_scan: false,
address_scan_content: CameraContent::default(),
modal_just_opened: false,
qr_address_content: QrCodeContent::new(addr),
qr_address_content: QrCodeContent::new(addr, false),
tor_settings_changed: false,
bridge_bin_path_edit: bin_path,
bridge_conn_line_edit: conn_line,
@ -327,7 +327,7 @@ impl WalletTransport {
};
if let Some(result) = self.bridge_qr_scan_content.qr_scan_result() {
self.bridge_conn_line_edit = result.value();
self.bridge_conn_line_edit = result.text();
on_stop(&mut self.bridge_qr_scan_content);
cb.show_keyboard();
} else {
@ -688,7 +688,7 @@ impl WalletTransport {
};
if let Some(result) = self.address_scan_content.qr_scan_result() {
self.address_edit = result.value();
self.address_edit = result.text();
self.modal_just_opened = true;
on_stop(&mut self.address_scan_content);
cb.show_keyboard();

View file

@ -80,7 +80,7 @@ impl Default for WalletTransactions {
tx_info_finalizing: false,
tx_info_final_result: Arc::new(RwLock::new(None)),
tx_info_show_qr: false,
tx_info_qr_code_content: QrCodeContent::new("".to_string()),
tx_info_qr_code_content: QrCodeContent::new("".to_string(), true),
tx_info_show_scanner: false,
tx_info_scanner_content: CameraContent::default(),
confirm_cancel_tx_id: None,
@ -708,7 +708,7 @@ impl WalletTransactions {
self.tx_info_scanner_content.clear_state();
// Setup value to finalization input field.
self.tx_info_finalize_edit = result.value();
self.tx_info_finalize_edit = result.text();
self.on_finalization_input_change(tx, wallet, modal, cb);
modal.enable_closing();
@ -737,7 +737,7 @@ impl WalletTransactions {
let desc_color = if self.tx_info_finalize_error {
Colors::RED
} else {
Colors::INACTIVE_TEXT
Colors::GRAY
};
ui.label(RichText::new(desc_text).size(16.0).color(desc_color));
} else {
@ -754,7 +754,7 @@ impl WalletTransactions {
t!("wallets.parse_s1_slatepack_desc", "amount" => amount)
}
};
ui.label(RichText::new(desc_text).size(16.0).color(Colors::INACTIVE_TEXT));
ui.label(RichText::new(desc_text).size(16.0).color(Colors::GRAY));
}
});
ui.add_space(6.0);

View file

@ -23,8 +23,6 @@ impl WalletUtils {
let mut hasher = Sha256::new();
hasher.update(data.clone());
let checksum = hasher.finalize();
println!("BEFORE data: {}, checksum: {}", data.len(), checksum.len());
data.extend(checksum);
println!("AFTER data: {}", data.len());
}
}