ur: scanning and creating qr codes for slatepack messages
This commit is contained in:
parent
4b46e5a997
commit
3f03d145e8
10 changed files with 373 additions and 110 deletions
73
Cargo.lock
generated
73
Cargo.lock
generated
|
@ -1059,6 +1059,21 @@ version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
|
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]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
@ -1800,6 +1815,21 @@ dependencies = [
|
||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -3705,6 +3735,7 @@ dependencies = [
|
||||||
"tor-keymgr",
|
"tor-keymgr",
|
||||||
"tor-llcrypto",
|
"tor-llcrypto",
|
||||||
"tor-rtcompat",
|
"tor-rtcompat",
|
||||||
|
"ur",
|
||||||
"url",
|
"url",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"winit",
|
"winit",
|
||||||
|
@ -5492,6 +5523,26 @@ dependencies = [
|
||||||
"unicase",
|
"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]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -7102,6 +7153,15 @@ dependencies = [
|
||||||
"rand_core 0.3.1",
|
"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]]
|
[[package]]
|
||||||
name = "range-alloc"
|
name = "range-alloc"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
@ -10258,6 +10318,19 @@ version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
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]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
|
@ -57,6 +57,7 @@ tokio = { version = "1.37.0", features = ["full"] }
|
||||||
image = "0.25.1"
|
image = "0.25.1"
|
||||||
rqrr = "0.7.1"
|
rqrr = "0.7.1"
|
||||||
qrcodegen = "1.8.0"
|
qrcodegen = "1.8.0"
|
||||||
|
ur = "0.4.1"
|
||||||
|
|
||||||
## tor
|
## tor
|
||||||
arti = { version = "1.2.0", features = ["pt-client", "static"] }
|
arti = { version = "1.2.0", features = ["pt-client", "static"] }
|
||||||
|
|
|
@ -35,18 +35,26 @@ use crate::wallet::WalletUtils;
|
||||||
/// Camera QR code scanner.
|
/// Camera QR code scanner.
|
||||||
pub struct CameraContent {
|
pub struct CameraContent {
|
||||||
/// QR code scanning progress and result.
|
/// 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 {
|
impl Default for CameraContent {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
|
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
|
||||||
|
ur_data: Arc::new(RwLock::new(None)),
|
||||||
|
start: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CameraContent {
|
impl CameraContent {
|
||||||
|
/// Draw camera content.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||||
// Draw last image from camera or loader.
|
// Draw last image from camera or loader.
|
||||||
if let Some(img_data) = cb.camera_image() {
|
if let Some(img_data) = cb.camera_image() {
|
||||||
|
@ -138,8 +146,13 @@ impl CameraContent {
|
||||||
r_scan.image_processing
|
r_scan.image_processing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get UR scanning progress in percents.
|
||||||
|
fn ur_progress(&self) -> i32 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse QR code from provided image data.
|
/// 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.
|
// Do not scan when another image is processing.
|
||||||
if self.image_processing() {
|
if self.image_processing() {
|
||||||
return;
|
return;
|
||||||
|
@ -149,55 +162,117 @@ impl CameraContent {
|
||||||
let mut w_scan = self.qr_scan_state.write();
|
let mut w_scan = self.qr_scan_state.write();
|
||||||
w_scan.image_processing = true;
|
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();
|
let qr_scan_state = self.qr_scan_state.clone();
|
||||||
|
let ur_data = self.ur_data.clone();
|
||||||
|
|
||||||
|
let on_scan = async move {
|
||||||
|
// Prepare image data.
|
||||||
|
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 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.text()
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
text
|
||||||
|
};
|
||||||
|
// 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(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 || {
|
thread::spawn(move || {
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.block_on(async {
|
.block_on(on_scan);
|
||||||
// Prepare image data.
|
|
||||||
let img = 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 cur_text = {
|
|
||||||
let r_scan = qr_scan_state.read();
|
|
||||||
let text = if let Some(res) = r_scan.qr_scan_result.clone() {
|
|
||||||
res.value()
|
|
||||||
} 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);
|
|
||||||
let mut w_scan = qr_scan_state.write();
|
|
||||||
w_scan.qr_scan_result = Some(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Reset scanning flag to process again.
|
|
||||||
{
|
|
||||||
let mut w_scan = qr_scan_state.write();
|
|
||||||
w_scan.image_processing = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
// Check if string starts with Grin address prefix.
|
||||||
let text_string = String::from_utf8(data.clone()).unwrap_or("".to_string());
|
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 text.starts_with("tgrin") || text.starts_with("grin") {
|
||||||
if SlatepackAddress::try_from(text).is_ok() {
|
if SlatepackAddress::try_from(text).is_ok() {
|
||||||
return QrScanResult::Address(ZeroingString::from(text));
|
return QrScanResult::Address(ZeroingString::from(text));
|
||||||
|
@ -209,7 +284,25 @@ impl CameraContent {
|
||||||
return QrScanResult::Slatepack(ZeroingString::from(text));
|
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 {
|
if data.len() <= 32 && 16 <= data.len() && data.len() % 4 == 0 {
|
||||||
// Setup words amount.
|
// Setup words amount.
|
||||||
let total_bits = data.len() * 8;
|
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 = || {
|
let only_numbers = || {
|
||||||
for c in text.chars() {
|
for c in text.chars() {
|
||||||
if !c.is_numeric() {
|
if !c.is_numeric() {
|
||||||
|
@ -316,7 +410,11 @@ impl CameraContent {
|
||||||
|
|
||||||
/// Reset camera content state to default.
|
/// Reset camera content state to default.
|
||||||
pub fn clear_state(&mut self) {
|
pub fn clear_state(&mut self) {
|
||||||
|
// Clear QR code scanning state.
|
||||||
let mut w_scan = self.qr_scan_state.write();
|
let mut w_scan = self.qr_scan_state.write();
|
||||||
*w_scan = QrScanState::default();
|
*w_scan = QrScanState::default();
|
||||||
|
// Clear UR data.
|
||||||
|
let mut w_data = self.ur_data.write();
|
||||||
|
*w_data = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -28,16 +28,27 @@ pub struct QrCodeContent {
|
||||||
/// Text to create QR code.
|
/// Text to create QR code.
|
||||||
pub(crate) text: String,
|
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 to show image when created.
|
||||||
texture_handle: Option<TextureHandle>,
|
texture_handle: Option<TextureHandle>,
|
||||||
/// QR code image creation progress and result.
|
/// QR code image creation progress and result.
|
||||||
qr_creation_state: Arc<RwLock<QrCreationState>>
|
qr_creation_state: Arc<RwLock<QrCreationState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QrCodeContent {
|
impl QrCodeContent {
|
||||||
pub fn new(text: String) -> Self {
|
pub fn new(text: String, animated: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text,
|
text,
|
||||||
|
animated,
|
||||||
|
animated_index: None,
|
||||||
|
animation_time: None,
|
||||||
texture_handle: None,
|
texture_handle: None,
|
||||||
qr_creation_state: Arc::new(RwLock::new(QrCreationState::default())),
|
qr_creation_state: Arc::new(RwLock::new(QrCreationState::default())),
|
||||||
}
|
}
|
||||||
|
@ -45,67 +56,141 @@ impl QrCodeContent {
|
||||||
|
|
||||||
/// Draw QR code.
|
/// Draw QR code.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, text: String) {
|
pub fn ui(&mut self, ui: &mut egui::Ui, text: String) {
|
||||||
// Get saved QR code image or load new one.
|
if self.animated {
|
||||||
if !self.has_image() {
|
// Create animated QR code image if not created.
|
||||||
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
if !self.has_image() {
|
||||||
ui.vertical_centered(|ui| {
|
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
||||||
ui.add_space(space);
|
ui.vertical_centered(|ui| {
|
||||||
View::big_loading_spinner(ui);
|
ui.add_space(space);
|
||||||
ui.add_space(space);
|
View::big_loading_spinner(ui);
|
||||||
});
|
ui.add_space(space);
|
||||||
|
});
|
||||||
|
|
||||||
// Create image from text if not loading.
|
// Create multiple vector images from text if not creating.
|
||||||
self.create_image(text);
|
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 {
|
} else {
|
||||||
// Create image from SVG data.
|
// Create vector QR code image if not created.
|
||||||
let r_create = self.qr_creation_state.read();
|
if !self.has_image() {
|
||||||
let svg = r_create.svg.as_ref().unwrap();
|
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
||||||
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
|
ui.vertical_centered(|ui| {
|
||||||
let color_img = load_svg_bytes_with_size(svg, Some(size)).unwrap();
|
ui.add_space(space);
|
||||||
// Create image texture.
|
View::big_loading_spinner(ui);
|
||||||
let texture_handle = ui.ctx().load_texture("qr_code",
|
ui.add_space(space);
|
||||||
color_img.clone(),
|
});
|
||||||
TextureOptions::default());
|
|
||||||
self.texture_handle = Some(texture_handle.clone());
|
// Create vector image from text if not creating.
|
||||||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
if !self.creating() {
|
||||||
color_img.height() as f32);
|
self.create_svg(text);
|
||||||
let sized_img = SizedTexture::new(texture_handle.id(), img_size);
|
}
|
||||||
// Add image to content.
|
} else {
|
||||||
ui.add(egui::Image::from_texture(sized_img)
|
// Create image from SVG data.
|
||||||
.max_height(ui.available_width())
|
let r_create = self.qr_creation_state.read();
|
||||||
.fit_to_original_size(1.0));
|
let svg = r_create.svg.as_ref().unwrap();
|
||||||
|
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
|
||||||
|
let color_img = load_svg_bytes_with_size(svg, 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if image is creating.
|
/// Check if QR code is creating.
|
||||||
fn creating(&self) -> bool {
|
fn creating(&self) -> bool {
|
||||||
let r_create = self.qr_creation_state.read();
|
let r_create = self.qr_creation_state.read();
|
||||||
r_create.creating
|
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.
|
/// Check if image was created.
|
||||||
fn has_image(&self) -> bool {
|
fn has_image(&self) -> bool {
|
||||||
let r_create = self.qr_creation_state.read();
|
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.
|
/// Create vector QR code image at separate thread.
|
||||||
fn create_image(&self, text: String) {
|
fn create_svg(&self, text: String) {
|
||||||
let qr_creation_state = self.qr_creation_state.clone();
|
let qr_creation_state = self.qr_creation_state.clone();
|
||||||
if !self.creating() {
|
thread::spawn(move || {
|
||||||
thread::spawn(move || {
|
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) {
|
||||||
if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Medium) {
|
let svg = Self::qr_to_svg(qr, 0);
|
||||||
let svg = Self::qr_to_svg(qr, 0);
|
let mut w_create = qr_creation_state.write();
|
||||||
let mut w_create = qr_creation_state.write();
|
w_create.creating = false;
|
||||||
w_create.creating = false;
|
w_create.svg = Some(svg.into_bytes());
|
||||||
w_create.svg = Some(svg.into_bytes());
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert QR code to SVG string.
|
/// 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 mut result = String::new();
|
||||||
let dimension = qr.size().checked_add(border.checked_mul(2).unwrap()).unwrap();
|
let dimension = qr.size().checked_add(border.checked_mul(2).unwrap()).unwrap();
|
||||||
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
||||||
|
|
|
@ -154,19 +154,24 @@ pub enum QrScanResult {
|
||||||
Address(ZeroingString),
|
Address(ZeroingString),
|
||||||
/// Parsed text.
|
/// Parsed text.
|
||||||
Text(ZeroingString),
|
Text(ZeroingString),
|
||||||
/// Parsed SeedQR https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md.
|
/// Recovery phrase in standard or compact SeedQR format.
|
||||||
SeedQR(ZeroingString)
|
/// 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 {
|
impl QrScanResult {
|
||||||
/// Get scan result value.
|
/// Get text scanning result.
|
||||||
pub fn value(&self) -> String {
|
pub fn text(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
QrScanResult::Slatepack(text) => text,
|
QrScanResult::Slatepack(text) => text.to_string(),
|
||||||
QrScanResult::Address(text) => text,
|
QrScanResult::Address(text) => text.to_string(),
|
||||||
QrScanResult::Text(text) => text,
|
QrScanResult::Text(text) => text.to_string(),
|
||||||
QrScanResult::SeedQR(text) => text
|
QrScanResult::SeedQR(text) => text.to_string(),
|
||||||
}.to_string()
|
QrScanResult::URPart(uri, _, _) => uri.to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,8 +196,10 @@ impl Default for QrScanState {
|
||||||
pub struct QrCreationState {
|
pub struct QrCreationState {
|
||||||
// Flag to check if QR code image is creating.
|
// Flag to check if QR code image is creating.
|
||||||
pub creating: bool,
|
pub creating: bool,
|
||||||
// Found QR code content.
|
// Vector image data.
|
||||||
pub svg: Option<Vec<u8>>
|
pub svg: Option<Vec<u8>>,
|
||||||
|
// Multiple vector image data.
|
||||||
|
pub svg_list: Option<Vec<Vec<u8>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for QrCreationState {
|
impl Default for QrCreationState {
|
||||||
|
@ -200,6 +207,7 @@ impl Default for QrCreationState {
|
||||||
Self {
|
Self {
|
||||||
creating: false,
|
creating: false,
|
||||||
svg: None,
|
svg: None,
|
||||||
|
svg_list: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -371,7 +371,7 @@ impl WalletContent {
|
||||||
cb: &dyn PlatformCallbacks) {
|
cb: &dyn PlatformCallbacks) {
|
||||||
// Show scan result if exists or show camera content while scanning.
|
// Show scan result if exists or show camera content while scanning.
|
||||||
if let Some(result) = &self.qr_scan_result {
|
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);
|
View::horizontal_line(ui, Colors::ITEM_STROKE);
|
||||||
ui.add_space(3.0);
|
ui.add_space(3.0);
|
||||||
ScrollArea::vertical()
|
ScrollArea::vertical()
|
||||||
|
|
|
@ -173,13 +173,13 @@ impl WalletMessages {
|
||||||
request_edit: "".to_string(),
|
request_edit: "".to_string(),
|
||||||
request_error: None,
|
request_error: None,
|
||||||
request_qr: false,
|
request_qr: false,
|
||||||
request_qr_content: QrCodeContent::new("".to_string()),
|
request_qr_content: QrCodeContent::new("".to_string(), true),
|
||||||
request_loading: false,
|
request_loading: false,
|
||||||
request_result: Arc::new(RwLock::new(None)),
|
request_result: Arc::new(RwLock::new(None)),
|
||||||
message_camera_content: CameraContent::default(),
|
message_camera_content: CameraContent::default(),
|
||||||
message_scan_error: false,
|
message_scan_error: false,
|
||||||
qr_message_text: None,
|
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 {
|
} else {
|
||||||
t!("wallets.send_request_desc","amount" => amount_format)
|
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);
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ impl WalletTransport {
|
||||||
show_address_scan: false,
|
show_address_scan: false,
|
||||||
address_scan_content: CameraContent::default(),
|
address_scan_content: CameraContent::default(),
|
||||||
modal_just_opened: false,
|
modal_just_opened: false,
|
||||||
qr_address_content: QrCodeContent::new(addr),
|
qr_address_content: QrCodeContent::new(addr, false),
|
||||||
tor_settings_changed: false,
|
tor_settings_changed: false,
|
||||||
bridge_bin_path_edit: bin_path,
|
bridge_bin_path_edit: bin_path,
|
||||||
bridge_conn_line_edit: conn_line,
|
bridge_conn_line_edit: conn_line,
|
||||||
|
@ -327,7 +327,7 @@ impl WalletTransport {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(result) = self.bridge_qr_scan_content.qr_scan_result() {
|
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);
|
on_stop(&mut self.bridge_qr_scan_content);
|
||||||
cb.show_keyboard();
|
cb.show_keyboard();
|
||||||
} else {
|
} else {
|
||||||
|
@ -688,7 +688,7 @@ impl WalletTransport {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(result) = self.address_scan_content.qr_scan_result() {
|
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;
|
self.modal_just_opened = true;
|
||||||
on_stop(&mut self.address_scan_content);
|
on_stop(&mut self.address_scan_content);
|
||||||
cb.show_keyboard();
|
cb.show_keyboard();
|
||||||
|
|
|
@ -80,7 +80,7 @@ impl Default for WalletTransactions {
|
||||||
tx_info_finalizing: false,
|
tx_info_finalizing: false,
|
||||||
tx_info_final_result: Arc::new(RwLock::new(None)),
|
tx_info_final_result: Arc::new(RwLock::new(None)),
|
||||||
tx_info_show_qr: false,
|
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_show_scanner: false,
|
||||||
tx_info_scanner_content: CameraContent::default(),
|
tx_info_scanner_content: CameraContent::default(),
|
||||||
confirm_cancel_tx_id: None,
|
confirm_cancel_tx_id: None,
|
||||||
|
@ -708,7 +708,7 @@ impl WalletTransactions {
|
||||||
self.tx_info_scanner_content.clear_state();
|
self.tx_info_scanner_content.clear_state();
|
||||||
|
|
||||||
// Setup value to finalization input field.
|
// 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);
|
self.on_finalization_input_change(tx, wallet, modal, cb);
|
||||||
|
|
||||||
modal.enable_closing();
|
modal.enable_closing();
|
||||||
|
@ -737,7 +737,7 @@ impl WalletTransactions {
|
||||||
let desc_color = if self.tx_info_finalize_error {
|
let desc_color = if self.tx_info_finalize_error {
|
||||||
Colors::RED
|
Colors::RED
|
||||||
} else {
|
} else {
|
||||||
Colors::INACTIVE_TEXT
|
Colors::GRAY
|
||||||
};
|
};
|
||||||
ui.label(RichText::new(desc_text).size(16.0).color(desc_color));
|
ui.label(RichText::new(desc_text).size(16.0).color(desc_color));
|
||||||
} else {
|
} else {
|
||||||
|
@ -754,7 +754,7 @@ impl WalletTransactions {
|
||||||
t!("wallets.parse_s1_slatepack_desc", "amount" => amount)
|
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);
|
ui.add_space(6.0);
|
||||||
|
|
|
@ -23,8 +23,6 @@ impl WalletUtils {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(data.clone());
|
hasher.update(data.clone());
|
||||||
let checksum = hasher.finalize();
|
let checksum = hasher.finalize();
|
||||||
println!("BEFORE data: {}, checksum: {}", data.len(), checksum.len());
|
|
||||||
data.extend(checksum);
|
data.extend(checksum);
|
||||||
println!("AFTER data: {}", data.len());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue