From 59d1d4521582d70dbd5765c4e0dd051b9aa685e7 Mon Sep 17 00:00:00 2001 From: John Wiegley Date: Fri, 7 Jan 2022 10:25:37 -0800 Subject: [PATCH 1/5] [IC-842] Add support for the merge_neurons management command (#89) * Add support for the merge_neurons management command * Use --merge-from-neuron * Increase version * Fix a clippy warning * Fix a test * In the merge neuron test, use different neuron ids * Update default.nix --- Cargo.lock | 2 +- Cargo.toml | 2 +- candid/governance.did | 4 ++++ default.nix | 4 ++-- src/commands/neuron_manage.rs | 23 +++++++++++++++++++++++ tests/commands/neuron-manage-merge.sh | 1 + tests/outputs/neuron-manage-merge.txt | 17 +++++++++++++++++ 7 files changed, 49 insertions(+), 4 deletions(-) create mode 100755 tests/commands/neuron-manage-merge.sh create mode 100644 tests/outputs/neuron-manage-merge.txt diff --git a/Cargo.lock b/Cargo.lock index eb604df3..b71e1633 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2082,7 +2082,7 @@ dependencies = [ [[package]] name = "quill" -version = "0.2.13" +version = "0.2.14" dependencies = [ "anyhow", "bip39", diff --git a/Cargo.toml b/Cargo.toml index 9afef7fd..0811eda1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quill" -version = "0.2.13" +version = "0.2.14" authors = ["DFINITY Team"] edition = "2018" diff --git a/candid/governance.did b/candid/governance.did index 7e1c5a33..350c7773 100644 --- a/candid/governance.did +++ b/candid/governance.did @@ -36,6 +36,7 @@ type Command = variant { ClaimOrRefresh : ClaimOrRefresh; Configure : Configure; RegisterVote : RegisterVote; + Merge : Merge; DisburseToNeuron : DisburseToNeuron; MakeProposal : Proposal; MergeMaturity : MergeMaturity; @@ -49,6 +50,7 @@ type Command_1 = variant { ClaimOrRefresh : ClaimOrRefreshResponse; Configure : record {}; RegisterVote : record {}; + Merge : record {}; DisburseToNeuron : SpawnResponse; MakeProposal : MakeProposalResponse; MergeMaturity : MergeMaturityResponse; @@ -57,6 +59,7 @@ type Command_1 = variant { type Command_2 = variant { Spawn : Spawn; Split : Split; + Merge : Merge; DisburseToNeuron : DisburseToNeuron; ClaimOrRefreshNeuron : ClaimOrRefresh; MergeMaturity : MergeMaturity; @@ -123,6 +126,7 @@ type ManageNeuron = record { neuron_id_or_subaccount : opt NeuronIdOrSubaccount; }; type ManageNeuronResponse = record { command : opt Command_1 }; +type Merge = record { source_neuron_id : opt NeuronId }; type MergeMaturity = record { percentage_to_merge : nat32 }; type MergeMaturityResponse = record { merged_maturity_e8s : nat64; diff --git a/default.nix b/default.nix index fe201009..9e91d3ab 100644 --- a/default.nix +++ b/default.nix @@ -10,11 +10,11 @@ with pkgs; rustPlatform.buildRustPackage rec { pname = "quill"; - version = "0.2.9"; + version = "0.2.14"; src = ./.; - cargoSha256 = "1x0gsg62nmf5l9avfd18qzdc9i9lm2y62qgkj9iwshgdfjqzavvy"; + cargoSha256 = "sha256-IDPAYnCTvYGUXjqJjN6hbmkKTPwlV2QaLADUyZBpH48="; cargoBuildFlags = []; diff --git a/src/commands/neuron_manage.rs b/src/commands/neuron_manage.rs index 98dbf823..ef895218 100644 --- a/src/commands/neuron_manage.rs +++ b/src/commands/neuron_manage.rs @@ -84,6 +84,11 @@ pub struct Split { pub amount_e8s: u64, } +#[derive(CandidType)] +pub struct Merge { + pub source_neuron_id: NeuronId, +} + #[derive(candid::CandidType)] pub struct MergeMaturity { pub percentage_to_merge: u32, @@ -95,6 +100,7 @@ pub enum Command { Disburse(Disburse), Spawn(Spawn), Split(Split), + Merge(Merge), MergeMaturity(MergeMaturity), } @@ -143,6 +149,10 @@ pub struct ManageOpts { #[clap(long)] split: Option, + /// Merge stake, maturity and age from the neuron specified by this option into the neuron being managed. + #[clap(long)] + merge_from_neuron: Option, + /// Merge the percentage (between 1 and 100) of the maturity of a neuron into the current stake. #[clap(long)] merge_maturity: Option, @@ -283,6 +293,19 @@ pub fn exec(pem: &str, opts: ManageOpts) -> AnyhowResult 100 { return Err(anyhow!( diff --git a/tests/commands/neuron-manage-merge.sh b/tests/commands/neuron-manage-merge.sh new file mode 100755 index 00000000..2958d90a --- /dev/null +++ b/tests/commands/neuron-manage-merge.sh @@ -0,0 +1 @@ +${CARGO_TARGET_DIR:-../target}/debug/quill --pem-file - neuron-manage 2313380519530470538 --merge-from-neuron 380519530470538 | ${CARGO_TARGET_DIR:-../target}/debug/quill send --dry-run - diff --git a/tests/outputs/neuron-manage-merge.txt b/tests/outputs/neuron-manage-merge.txt new file mode 100644 index 00000000..98224e8a --- /dev/null +++ b/tests/outputs/neuron-manage-merge.txt @@ -0,0 +1,17 @@ +Sending message with + + Call type: update + Sender: fdsgv-62ihb-nbiqv-xgic5-iefsv-3cscz-tmbzv-63qd5-vh43v-dqfrt-pae + Canister id: rrkah-fqaaa-aaaaa-aaaaq-cai + Method name: manage_neuron + Arguments: ( + record { + id = opt record { id = 2_313_380_519_530_470_538 : nat64 }; + command = opt variant { + Merge = record { + source_neuron_id = opt record { id = 380_519_530_470_538 : nat64 }; + } + }; + neuron_id_or_subaccount = null; + }, +) From c94a4311fa6d2ac8e16eef20080ea5dc1e03f8c1 Mon Sep 17 00:00:00 2001 From: John Wiegley Date: Sat, 8 Jan 2022 10:06:15 -0800 Subject: [PATCH 2/5] Update default.nix --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 9e91d3ab..f92fa57e 100644 --- a/default.nix +++ b/default.nix @@ -14,7 +14,7 @@ with pkgs; rustPlatform.buildRustPackage rec { src = ./.; - cargoSha256 = "sha256-IDPAYnCTvYGUXjqJjN6hbmkKTPwlV2QaLADUyZBpH48="; + cargoSha256 = "sha256-zFRMDnSNOVDCh+cupe7ZieR5UPrwHDZ9oi7MnzWpk2s="; cargoBuildFlags = []; From b41a4e8b86f011cdc6ba2950f7b4b71e9ddbe92a Mon Sep 17 00:00:00 2001 From: John Plevyak Date: Sat, 8 Jan 2022 22:31:42 -0800 Subject: [PATCH 3/5] Add support for generating QR codes directly. (#92) * Add support for generating QR codes directly including a command to output QR scanner QR code and a command for generating arbitrary QR codes to enable transfer of the seed phrase and principal id. Signed-off-by: John Plevyak --- Cargo.lock | 30 + Cargo.toml | 2 + src/commands/mod.rs | 79 ++- src/commands/qrcode.rs | 26 + src/lib/mod.rs | 1 + src/lib/qr.rs | 1323 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 6 +- 7 files changed, 1459 insertions(+), 8 deletions(-) create mode 100644 src/commands/qrcode.rs create mode 100644 src/lib/qr.rs diff --git a/Cargo.lock b/Cargo.lock index b71e1633..6cbfcdcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.18" @@ -663,6 +669,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1601,6 +1619,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg 1.0.1", +] + [[package]] name = "mio" version = "0.7.14" @@ -2085,9 +2113,11 @@ name = "quill" version = "0.2.14" dependencies = [ "anyhow", + "base64 0.13.0", "bip39", "candid", "clap", + "flate2", "hex", "ic-agent", "ic-base-types", diff --git a/Cargo.toml b/Cargo.toml index 0811eda1..eee111d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,11 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.34" +base64 = "0.13.0" bip39 = "1.0.1" candid = "0.7.8" clap = "3.0.0-beta.5" +flate2 = "1.0.22" hex = {version = "0.4.2", features = ["serde"] } ic-agent = "0.10.0" ic-base-types = { git = "https://github.com/dfinity/ic", rev = "bd3b73e075aea1cc81b23b38ccfb138ca4ab17ab" } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a3e372dd..50126cab 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,6 +1,6 @@ //! This module implements the command-line API. -use crate::lib::{require_pem, AnyhowResult}; +use crate::lib::{qr, require_pem, AnyhowResult}; use clap::Parser; use std::io::{self, Write}; use tokio::runtime::Runtime; @@ -14,6 +14,7 @@ mod list_proposals; mod neuron_manage; mod neuron_stake; mod public; +mod qrcode; mod request_status; mod send; mod transfer; @@ -38,31 +39,35 @@ pub enum Command { AccountBalance(account_balance::AccountBalanceOpts), /// Generate a mnemonic seed phrase and generate or recover PEM. Generate(generate::GenerateOpts), + /// Print QR Scanner dapp QR code: scan to start dapp to submit QR results. + ScannerQRCode, + /// Print QR code for data e.g. principal id. + QRCode(qrcode::QRCodeOpts), } -pub fn exec(pem: &Option, cmd: Command) -> AnyhowResult { +pub fn exec(pem: &Option, qr: bool, cmd: Command) -> AnyhowResult { let runtime = Runtime::new().expect("Unable to create a runtime"); match cmd { Command::PublicIds(opts) => public::exec(pem, opts), Command::Transfer(opts) => { let pem = require_pem(pem)?; - transfer::exec(&pem, opts).and_then(|out| print(&out)) + transfer::exec(&pem, opts).and_then(|out| print_vec(qr, &out)) } Command::NeuronStake(opts) => { let pem = require_pem(pem)?; - neuron_stake::exec(&pem, opts).and_then(|out| print(&out)) + neuron_stake::exec(&pem, opts).and_then(|out| print_vec(qr, &out)) } Command::NeuronManage(opts) => { let pem = require_pem(pem)?; - neuron_manage::exec(&pem, opts).and_then(|out| print(&out)) + neuron_manage::exec(&pem, opts).and_then(|out| print_vec(qr, &out)) } Command::ListNeurons(opts) => { let pem = require_pem(pem)?; - list_neurons::exec(&pem, opts).and_then(|out| print(&out)) + list_neurons::exec(&pem, opts).and_then(|out| print_vec(qr, &out)) } Command::ClaimNeurons => { let pem = require_pem(pem)?; - claim_neurons::exec(&pem).and_then(|out| print(&out)) + claim_neurons::exec(&pem).and_then(|out| print_vec(qr, &out)) } Command::ListProposals(opts) => { runtime.block_on(async { list_proposals::exec(opts).await }) @@ -75,6 +80,33 @@ pub fn exec(pem: &Option, cmd: Command) -> AnyhowResult { } Command::Send(opts) => runtime.block_on(async { send::exec(opts).await }), Command::Generate(opts) => generate::exec(opts), + // QR code for URL: https://p5deo-6aaaa-aaaab-aaaxq-cai.raw.ic0.app/ + // Source code: https://github.com/ninegua/ic-qr-scanner + Command::ScannerQRCode => { + println!( + "█████████████████████████████████████ +█████████████████████████████████████ +████ ▄▄▄▄▄ █▀█ █▄▀▄▀▄█ ▄ █ ▄▄▄▄▄ ████ +████ █ █ █▀▀▀█ ▀▀█▄▀████ █ █ ████ +████ █▄▄▄█ █▀ █▀▀██▀▀█ ▄ █ █▄▄▄█ ████ +████▄▄▄▄▄▄▄█▄▀ ▀▄█ ▀▄█▄█▄█▄▄▄▄▄▄▄████ +████▄▄▄▄ ▀▄ ▄▀▄ ▄ █▀▄▀▀▀ ▀ ▀▄█▄▀████ +████▄█ █ ▄█▀█▄▀█▄ ▄▄ █ █ ▀█▀█████ +████▄▀ ▀ █▄▄▄ ▄ █▄▀ █ ▀▀▀▄▄█▀████ +████▄██▀▄▀▄▄ █▀█ ▄▄▄▄███▄█▄▀ ▄▄▀█████ +████ ▀▄▀▄█▄▀▄▄▄▀█ ▄▄▀▄▀▀▀▄▀▀▀▄ █▀████ +████ █▀██▀▄██▀▄█ █▀ █▄█▄▀▀ █▄▀█████ +████▄████▄▄▄ ▀▀█▄▄██▄▀█ ▄▄▄ ▀ ████ +████ ▄▄▄▄▄ █▄▄██▀▄▀ ▄█▄ █▄█ ▄▄▀█████ +████ █ █ █ █▀▄▄▀▄ ▄▀▀▄▄▄ ▄▀ ▄▀████ +████ █▄▄▄█ █ █▄▀▄██ ██▄█▀ ▄█ ▄ █████ +████▄▄▄▄▄▄▄█▄▄▄▄▄▄██▄▄█▄████▄▄▄██████ +█████████████████████████████████████ +█████████████████████████████████████" + ); + Ok(()) + } + Command::QRCode(opts) => qrcode::exec(opts), } } @@ -94,3 +126,36 @@ where } Ok(()) } + +fn print_qr(arg: &T, pause: bool) -> AnyhowResult +where + T: serde::ser::Serialize, +{ + let json = serde_json::to_string(&arg)?; + let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + e.write_all(json.as_bytes()).unwrap(); + let json = e.finish().unwrap(); + let json = base64::encode(json); + qr::print_qr(json.as_str()); + if pause { + let mut input_string = String::new(); + std::io::stdin() + .read_line(&mut input_string) + .expect("Failed to read line"); + } + Ok(()) +} + +fn print_vec(qr: bool, arg: &[T]) -> AnyhowResult +where + T: serde::ser::Serialize, +{ + if !qr { + print(arg) + } else { + for (i, a) in arg.iter().enumerate() { + print_qr(&a, i != arg.len() - 1).expect("print_qr"); + } + Ok(()) + } +} diff --git a/src/commands/qrcode.rs b/src/commands/qrcode.rs new file mode 100644 index 00000000..c19e4ecd --- /dev/null +++ b/src/commands/qrcode.rs @@ -0,0 +1,26 @@ +use crate::lib::qr::print_qr; +use crate::lib::{read_from_file, AnyhowResult}; +use clap::Parser; + +#[derive(Parser)] +pub struct QRCodeOpts { + /// File the contents of which to be output as a QRCode. + #[clap(long)] + file: Option, + + // String to be output as a QRCode. + #[clap(long)] + string: Option, +} + +/// Prints the account and the principal ids. +pub fn exec(opts: QRCodeOpts) -> AnyhowResult { + if let Some(file) = opts.file { + let data = read_from_file(&file)?; + print_qr(&data); + } + if let Some(string) = opts.string { + print_qr(&string); + } + Ok(()) +} diff --git a/src/lib/mod.rs b/src/lib/mod.rs index e0f1dd37..fff06b5f 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -28,6 +28,7 @@ pub fn get_ic_url() -> String { std::env::var("IC_URL").unwrap_or_else(|_| IC_URL.to_string()) } +pub mod qr; pub mod signing; pub type AnyhowResult = anyhow::Result; diff --git a/src/lib/qr.rs b/src/lib/qr.rs new file mode 100644 index 00000000..1d78c6e5 --- /dev/null +++ b/src/lib/qr.rs @@ -0,0 +1,1323 @@ +/* + * QR Code generator library (Rust) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +//! Generates QR Codes from text strings and byte arrays. +//! +//! This project aims to be the best, clearest QR Code generator library. +//! The primary goals are flexible options and absolute correctness. +//! Secondary goals are compact implementation size and good documentation comments. +//! +//! Home page with live JavaScript demo, extensive descriptions, and competitor comparisons: +//! [https://www.nayuki.io/page/qr-code-generator-library](https://www.nayuki.io/page/qr-code-generator-library) +//! +//! # Features +//! +//! Core features: +//! +//! - Significantly shorter code but more documentation comments compared to competing libraries +//! - Supports encoding all 40 versions (sizes) and all 4 error correction levels, as per the QR Code Model 2 standard +//! - Output format: Raw modules/pixels of the QR symbol +//! - Detects finder-like penalty patterns more accurately than other implementations +//! - Encodes numeric and special-alphanumeric text in less space than general text +//! - Open-source code under the permissive MIT License +//! +//! Manual parameters: +//! +//! - User can specify minimum and maximum version numbers allowed, then library will automatically choose smallest version in the range that fits the data +//! - User can specify mask pattern manually, otherwise library will automatically evaluate all 8 masks and select the optimal one +//! - User can specify absolute error correction level, or allow the library to boost it if it doesn't increase the version number +//! - User can create a list of data segments manually and add ECI segments +//! +//! More information about QR Code technology and this library's design can be found on the project home page. +//! +//! # Examples +//! +//! ``` +//! extern crate qrcodegen; +//! use qrcodegen::Mask; +//! use qrcodegen::QrCode; +//! use qrcodegen::QrCodeEcc; +//! use qrcodegen::QrSegment; +//! use qrcodegen::Version; +//! ``` +//! +//! Simple operation: +//! +//! ``` +//! let qr = QrCode::encode_text("Hello, world!", +//! QrCodeEcc::Medium).unwrap(); +//! let svg = to_svg_string(&qr, 4); // See qrcodegen-demo +//! ``` +//! +//! Manual operation: +//! +//! ``` +//! let text: &str = "3141592653589793238462643383"; +//! let segs = QrSegment::make_segments(text); +//! let qr = QrCode::encode_segments_advanced(&segs, QrCodeEcc::High, +//! Version::new(5), Version::new(5), Some(Mask::new(2)), false).unwrap(); +//! for y in 0 .. qr.size() { +//! for x in 0 .. qr.size() { +//! (... paint qr.get_module(x, y) ...) +//! } +//! } +//! ``` +#![allow(dead_code)] +#![allow(unused_variables)] +use std::convert::TryFrom; + +/*---- QrCode functionality ----*/ + +/// A QR Code symbol, which is a type of two-dimension barcode. +/// +/// Invented by Denso Wave and described in the ISO/IEC 18004 standard. +/// +/// Instances of this struct represent an immutable square grid of dark and light cells. +/// The impl provides static factory functions to create a QR Code from text or binary data. +/// The struct and impl cover the QR Code Model 2 specification, supporting all versions +/// (sizes) from 1 to 40, all 4 error correction levels, and 4 character encoding modes. +/// +/// Ways to create a QR Code object: +/// +/// - High level: Take the payload data and call `QrCode::encode_text()` or `QrCode::encode_binary()`. +/// - Mid level: Custom-make the list of segments and call +/// `QrCode::encode_segments()` or `QrCode::encode_segments_advanced()`. +/// - Low level: Custom-make the array of data codeword bytes (including segment +/// headers and final padding, excluding error correction codewords), supply the +/// appropriate version number, and call the `QrCode::encode_codewords()` constructor. +/// +/// (Note that all ways require supplying the desired error correction level.) +#[derive(Clone, PartialEq, Eq)] +pub struct QrCode { + // Scalar parameters: + + // The version number of this QR Code, which is between 1 and 40 (inclusive). + // This determines the size of this barcode. + version: Version, + + // The width and height of this QR Code, measured in modules, between + // 21 and 177 (inclusive). This is equal to version * 4 + 17. + size: i32, + + // The error correction level used in this QR Code. + errorcorrectionlevel: QrCodeEcc, + + // The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive). + // Even if a QR Code is created with automatic masking requested (mask = None), + // the resulting object still has a mask value between 0 and 7. + mask: Mask, + + // Grids of modules/pixels, with dimensions of size*size: + + // The modules of this QR Code (false = light, true = dark). + // Immutable after constructor finishes. Accessed through get_module(). + modules: Vec, + + // Indicates function modules that are not subjected to masking. Discarded when constructor finishes. + isfunction: Vec, +} + +impl QrCode { + /*---- Static factory functions (high level) ----*/ + + /// Returns a QR Code representing the given Unicode text string at the given error correction level. + /// + /// As a conservative upper bound, this function is guaranteed to succeed for strings that have 738 or fewer Unicode + /// code points (not UTF-8 code units) if the low error correction level is used. The smallest possible + /// QR Code version is automatically chosen for the output. The ECC level of the result may be higher than + /// the ecl argument if it can be done without increasing the version. + /// + /// Returns a wrapped `QrCode` if successful, or `Err` if the + /// data is too long to fit in any version at the given ECC level. + pub fn encode_text(text: &str, ecl: QrCodeEcc) -> Result { + let segs: Vec = QrSegment::make_segments(text); + QrCode::encode_segments(&segs, ecl) + } + + /// Returns a QR Code representing the given binary data at the given error correction level. + /// + /// This function always encodes using the binary segment mode, not any text mode. The maximum number of + /// bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output. + /// The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version. + /// + /// Returns a wrapped `QrCode` if successful, or `Err` if the + /// data is too long to fit in any version at the given ECC level. + pub fn encode_binary(data: &[u8], ecl: QrCodeEcc) -> Result { + let segs: [QrSegment; 1] = [QrSegment::make_bytes(data)]; + QrCode::encode_segments(&segs, ecl) + } + + /*---- Static factory functions (mid level) ----*/ + + /// Returns a QR Code representing the given segments at the given error correction level. + /// + /// The smallest possible QR Code version is automatically chosen for the output. The ECC level + /// of the result may be higher than the ecl argument if it can be done without increasing the version. + /// + /// This function allows the user to create a custom sequence of segments that switches + /// between modes (such as alphanumeric and byte) to encode text in less space. + /// This is a mid-level API; the high-level API is `encode_text()` and `encode_binary()`. + /// + /// Returns a wrapped `QrCode` if successful, or `Err` if the + /// data is too long to fit in any version at the given ECC level. + pub fn encode_segments(segs: &[QrSegment], ecl: QrCodeEcc) -> Result { + QrCode::encode_segments_advanced(segs, ecl, Version::MIN, Version::MAX, None, true) + } + + /// Returns a QR Code representing the given segments with the given encoding parameters. + /// + /// The smallest possible QR Code version within the given range is automatically + /// chosen for the output. Iff boostecl is `true`, then the ECC level of the result + /// may be higher than the ecl argument if it can be done without increasing the + /// version. The mask number is either between 0 to 7 (inclusive) to force that + /// mask, or `None` to automatically choose an appropriate mask (which may be slow). + /// + /// This function allows the user to create a custom sequence of segments that switches + /// between modes (such as alphanumeric and byte) to encode text in less space. + /// This is a mid-level API; the high-level API is `encode_text()` and `encode_binary()`. + /// + /// Returns a wrapped `QrCode` if successful, or `Err` if the data is too + /// long to fit in any version in the given range at the given ECC level. + pub fn encode_segments_advanced( + segs: &[QrSegment], + mut ecl: QrCodeEcc, + minversion: Version, + maxversion: Version, + mask: Option, + boostecl: bool, + ) -> Result { + assert!(minversion <= maxversion, "Invalid value"); + + // Find the minimal version number to use + let mut version: Version = minversion; + let datausedbits: usize = loop { + let datacapacitybits: usize = QrCode::get_num_data_codewords(version, ecl) * 8; // Number of data bits available + let dataused: Option = QrSegment::get_total_bits(segs, version); + if dataused.map_or(false, |n| n <= datacapacitybits) { + break dataused.unwrap(); // This version number is found to be suitable + } else if version >= maxversion { + // All versions in the range could not fit the given data + return Err(match dataused { + None => DataTooLong::SegmentTooLong, + Some(n) => DataTooLong::DataOverCapacity(n, datacapacitybits), + }); + } else { + version = Version::new(version.value() + 1); + } + }; + + // Increase the error correction level while the data still fits in the current version number + for &newecl in &[QrCodeEcc::Medium, QrCodeEcc::Quartile, QrCodeEcc::High] { + // From low to high + if boostecl && datausedbits <= QrCode::get_num_data_codewords(version, newecl) * 8 { + ecl = newecl; + } + } + + // Concatenate all segments to create the data bit string + let mut bb = BitBuffer(Vec::new()); + for seg in segs { + bb.append_bits(seg.mode.mode_bits(), 4); + bb.append_bits( + u32::try_from(seg.numchars).unwrap(), + seg.mode.num_char_count_bits(version), + ); + bb.0.extend_from_slice(&seg.data); + } + debug_assert_eq!(bb.0.len(), datausedbits); + + // Add terminator and pad up to a byte if applicable + let datacapacitybits: usize = QrCode::get_num_data_codewords(version, ecl) * 8; + debug_assert!(bb.0.len() <= datacapacitybits); + let numzerobits: usize = std::cmp::min(4, datacapacitybits - bb.0.len()); + bb.append_bits(0, u8::try_from(numzerobits).unwrap()); + let numzerobits: usize = bb.0.len().wrapping_neg() & 7; + bb.append_bits(0, u8::try_from(numzerobits).unwrap()); + debug_assert_eq!(bb.0.len() % 8, 0); + + // Pad with alternating bytes until data capacity is reached + for &padbyte in [0xEC, 0x11].iter().cycle() { + if bb.0.len() >= datacapacitybits { + break; + } + bb.append_bits(padbyte, 8); + } + + // Pack bits into bytes in big endian + let mut datacodewords = vec![0u8; bb.0.len() / 8]; + for (i, &bit) in bb.0.iter().enumerate() { + datacodewords[i >> 3] |= u8::from(bit) << (7 - (i & 7)); + } + + // Create the QR Code object + Ok(QrCode::encode_codewords(version, ecl, &datacodewords, mask)) + } + + /*---- Constructor (low level) ----*/ + + /// Creates a new QR Code with the given version number, + /// error correction level, data codeword bytes, and mask number. + /// + /// This is a low-level API that most users should not use directly. + /// A mid-level API is the `encode_segments()` function. + pub fn encode_codewords( + ver: Version, + ecl: QrCodeEcc, + datacodewords: &[u8], + mut msk: Option, + ) -> Self { + // Initialize fields + let size = usize::from(ver.value()) * 4 + 17; + let mut result = Self { + version: ver, + size: size as i32, + mask: Mask::new(0), // Dummy value + errorcorrectionlevel: ecl, + modules: vec![false; size * size], // Initially all light + isfunction: vec![false; size * size], + }; + + // Compute ECC, draw modules + result.draw_function_patterns(); + let allcodewords: Vec = result.add_ecc_and_interleave(datacodewords); + result.draw_codewords(&allcodewords); + + // Do masking + if msk.is_none() { + // Automatically choose best mask + let mut minpenalty = std::i32::MAX; + for i in 0u8..8 { + let i = Mask::new(i); + result.apply_mask(i); + result.draw_format_bits(i); + let penalty: i32 = result.get_penalty_score(); + if penalty < minpenalty { + msk = Some(i); + minpenalty = penalty; + } + result.apply_mask(i); // Undoes the mask due to XOR + } + } + let msk: Mask = msk.unwrap(); + result.mask = msk; + result.apply_mask(msk); // Apply the final choice of mask + result.draw_format_bits(msk); // Overwrite old format bits + + result.isfunction.clear(); + result.isfunction.shrink_to_fit(); + result + } + + /*---- Public methods ----*/ + + /// Returns this QR Code's version, in the range [1, 40]. + pub fn version(&self) -> Version { + self.version + } + + /// Returns this QR Code's size, in the range [21, 177]. + pub fn size(&self) -> i32 { + self.size + } + + /// Returns this QR Code's error correction level. + pub fn error_correction_level(&self) -> QrCodeEcc { + self.errorcorrectionlevel + } + + /// Returns this QR Code's mask, in the range [0, 7]. + pub fn mask(&self) -> Mask { + self.mask + } + + /// Returns the color of the module (pixel) at the given coordinates, + /// which is `false` for light or `true` for dark. + /// + /// The top left corner has the coordinates (x=0, y=0). If the given + /// coordinates are out of bounds, then `false` (light) is returned. + pub fn get_module(&self, x: i32, y: i32) -> bool { + (0..self.size).contains(&x) && (0..self.size).contains(&y) && self.module(x, y) + } + + // Returns the color of the module at the given coordinates, which must be in bounds. + fn module(&self, x: i32, y: i32) -> bool { + self.modules[(y * self.size + x) as usize] + } + + // Returns a mutable reference to the module's color at the given coordinates, which must be in bounds. + fn module_mut(&mut self, x: i32, y: i32) -> &mut bool { + &mut self.modules[(y * self.size + x) as usize] + } + + /*---- Private helper methods for constructor: Drawing function modules ----*/ + + // Reads this object's version field, and draws and marks all function modules. + fn draw_function_patterns(&mut self) { + // Draw horizontal and vertical timing patterns + let size: i32 = self.size; + for i in 0..size { + self.set_function_module(6, i, i % 2 == 0); + self.set_function_module(i, 6, i % 2 == 0); + } + + // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) + self.draw_finder_pattern(3, 3); + self.draw_finder_pattern(size - 4, 3); + self.draw_finder_pattern(3, size - 4); + + // Draw numerous alignment patterns + let alignpatpos: Vec = self.get_alignment_pattern_positions(); + let numalign: usize = alignpatpos.len(); + for i in 0..numalign { + for j in 0..numalign { + // Don't draw on the three finder corners + if !(i == 0 && j == 0 || i == 0 && j == numalign - 1 || i == numalign - 1 && j == 0) + { + self.draw_alignment_pattern(alignpatpos[i], alignpatpos[j]); + } + } + } + + // Draw configuration data + self.draw_format_bits(Mask::new(0)); // Dummy mask value; overwritten later in the constructor + self.draw_version(); + } + + // Draws two copies of the format bits (with its own error correction code) + // based on the given mask and this object's error correction level field. + fn draw_format_bits(&mut self, mask: Mask) { + // Calculate error correction code and pack bits + let bits: u32 = { + // errcorrlvl is uint2, mask is uint3 + let data: u32 = u32::from(self.errorcorrectionlevel.format_bits() << 3 | mask.value()); + let mut rem: u32 = data; + for _ in 0..10 { + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + } + (data << 10 | rem) ^ 0x5412 // uint15 + }; + debug_assert_eq!(bits >> 15, 0); + + // Draw first copy + for i in 0..6 { + self.set_function_module(8, i, get_bit(bits, i)); + } + self.set_function_module(8, 7, get_bit(bits, 6)); + self.set_function_module(8, 8, get_bit(bits, 7)); + self.set_function_module(7, 8, get_bit(bits, 8)); + for i in 9..15 { + self.set_function_module(14 - i, 8, get_bit(bits, i)); + } + + // Draw second copy + let size: i32 = self.size; + for i in 0..8 { + self.set_function_module(size - 1 - i, 8, get_bit(bits, i)); + } + for i in 8..15 { + self.set_function_module(8, size - 15 + i, get_bit(bits, i)); + } + self.set_function_module(8, size - 8, true); // Always dark + } + + // Draws two copies of the version bits (with its own error correction code), + // based on this object's version field, iff 7 <= version <= 40. + fn draw_version(&mut self) { + if self.version.value() < 7 { + return; + } + + // Calculate error correction code and pack bits + let bits: u32 = { + let data = u32::from(self.version.value()); // uint6, in the range [7, 40] + let mut rem: u32 = data; + for _ in 0..12 { + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + } + data << 12 | rem // uint18 + }; + debug_assert_eq!(bits >> 18, 0); + + // Draw two copies + for i in 0..18 { + let bit: bool = get_bit(bits, i); + let a: i32 = self.size - 11 + i % 3; + let b: i32 = i / 3; + self.set_function_module(a, b, bit); + self.set_function_module(b, a, bit); + } + } + + // Draws a 9*9 finder pattern including the border separator, + // with the center module at (x, y). Modules can be out of bounds. + fn draw_finder_pattern(&mut self, x: i32, y: i32) { + for dy in -4..=4 { + for dx in -4..=4 { + let xx: i32 = x + dx; + let yy: i32 = y + dy; + if (0..self.size).contains(&xx) && (0..self.size).contains(&yy) { + let dist: i32 = std::cmp::max(dx.abs(), dy.abs()); // Chebyshev/infinity norm + self.set_function_module(xx, yy, dist != 2 && dist != 4); + } + } + } + } + + // Draws a 5*5 alignment pattern, with the center module + // at (x, y). All modules must be in bounds. + fn draw_alignment_pattern(&mut self, x: i32, y: i32) { + for dy in -2..=2 { + for dx in -2..=2 { + self.set_function_module(x + dx, y + dy, std::cmp::max(dx.abs(), dy.abs()) != 1); + } + } + } + + // Sets the color of a module and marks it as a function module. + // Only used by the constructor. Coordinates must be in bounds. + fn set_function_module(&mut self, x: i32, y: i32, isdark: bool) { + *self.module_mut(x, y) = isdark; + self.isfunction[(y * self.size + x) as usize] = true; + } + + /*---- Private helper methods for constructor: Codewords and masking ----*/ + + // Returns a new byte string representing the given data with the appropriate error correction + // codewords appended to it, based on this object's version and error correction level. + fn add_ecc_and_interleave(&self, data: &[u8]) -> Vec { + let ver: Version = self.version; + let ecl: QrCodeEcc = self.errorcorrectionlevel; + assert_eq!( + data.len(), + QrCode::get_num_data_codewords(ver, ecl), + "Illegal argument" + ); + + // Calculate parameter numbers + let numblocks: usize = QrCode::table_get(&NUM_ERROR_CORRECTION_BLOCKS, ver, ecl); + let blockecclen: usize = QrCode::table_get(&ECC_CODEWORDS_PER_BLOCK, ver, ecl); + let rawcodewords: usize = QrCode::get_num_raw_data_modules(ver) / 8; + let numshortblocks: usize = numblocks - rawcodewords % numblocks; + let shortblocklen: usize = rawcodewords / numblocks; + + // Split data into blocks and append ECC to each block + let mut blocks = Vec::>::with_capacity(numblocks); + let rsdiv: Vec = QrCode::reed_solomon_compute_divisor(blockecclen); + let mut k: usize = 0; + for i in 0..numblocks { + let datlen: usize = shortblocklen - blockecclen + usize::from(i >= numshortblocks); + let mut dat = data[k..k + datlen].to_vec(); + k += datlen; + let ecc: Vec = QrCode::reed_solomon_compute_remainder(&dat, &rsdiv); + if i < numshortblocks { + dat.push(0); + } + dat.extend_from_slice(&ecc); + blocks.push(dat); + } + + // Interleave (not concatenate) the bytes from every block into a single sequence + let mut result = Vec::::with_capacity(rawcodewords); + for i in 0..=shortblocklen { + for (j, block) in blocks.iter().enumerate() { + // Skip the padding byte in short blocks + if i != shortblocklen - blockecclen || j >= numshortblocks { + result.push(block[i]); + } + } + } + result + } + + // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire + // data area of this QR Code. Function modules need to be marked off before this is called. + fn draw_codewords(&mut self, data: &[u8]) { + assert_eq!( + data.len(), + QrCode::get_num_raw_data_modules(self.version) / 8, + "Illegal argument" + ); + + let mut i: usize = 0; // Bit index into the data + // Do the funny zigzag scan + let mut right: i32 = self.size - 1; + while right >= 1 { + // Index of right column in each column pair + if right == 6 { + right = 5; + } + for vert in 0..self.size { + // Vertical counter + for j in 0..2 { + let x: i32 = right - j; // Actual x coordinate + let upward: bool = (right + 1) & 2 == 0; + let y: i32 = if upward { self.size - 1 - vert } else { vert }; // Actual y coordinate + if !self.isfunction[(y * self.size + x) as usize] && i < data.len() * 8 { + *self.module_mut(x, y) = + get_bit(u32::from(data[i >> 3]), 7 - ((i as i32) & 7)); + i += 1; + } + // If this QR Code has any remainder bits (0 to 7), they were assigned as + // 0/false/light by the constructor and are left unchanged by this method + } + } + right -= 2; + } + debug_assert_eq!(i, data.len() * 8); + } + + // XORs the codeword modules in this QR Code with the given mask pattern. + // The function modules must be marked and the codeword bits must be drawn + // before masking. Due to the arithmetic of XOR, calling apply_mask() with + // the same mask value a second time will undo the mask. A final well-formed + // QR Code needs exactly one (not zero, two, etc.) mask applied. + fn apply_mask(&mut self, mask: Mask) { + for y in 0..self.size { + for x in 0..self.size { + let invert: bool = match mask.value() { + 0 => (x + y) % 2 == 0, + 1 => y % 2 == 0, + 2 => x % 3 == 0, + 3 => (x + y) % 3 == 0, + 4 => (x / 3 + y / 2) % 2 == 0, + 5 => x * y % 2 + x * y % 3 == 0, + 6 => (x * y % 2 + x * y % 3) % 2 == 0, + 7 => ((x + y) % 2 + x * y % 3) % 2 == 0, + _ => unreachable!(), + }; + *self.module_mut(x, y) ^= invert & !self.isfunction[(y * self.size + x) as usize]; + } + } + } + + // Calculates and returns the penalty score based on state of this QR Code's current modules. + // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. + fn get_penalty_score(&self) -> i32 { + let mut result: i32 = 0; + let size: i32 = self.size; + + // Adjacent modules in row having same color, and finder-like patterns + for y in 0..size { + let mut runcolor = false; + let mut runx: i32 = 0; + let mut runhistory = FinderPenalty::new(size); + for x in 0..size { + if self.module(x, y) == runcolor { + runx += 1; + match runx { + 5 => { + result += PENALTY_N1; + } + d if d > 5 => { + result += 1; + } + _ => {} + } + } else { + runhistory.add_history(runx); + if !runcolor { + result += runhistory.count_patterns() * PENALTY_N3; + } + runcolor = self.module(x, y); + runx = 1; + } + } + result += runhistory.terminate_and_count(runcolor, runx) * PENALTY_N3; + } + // Adjacent modules in column having same color, and finder-like patterns + for x in 0..size { + let mut runcolor = false; + let mut runy: i32 = 0; + let mut runhistory = FinderPenalty::new(size); + for y in 0..size { + if self.module(x, y) == runcolor { + runy += 1; + match runy { + 5 => { + result += PENALTY_N1; + } + d if d > 5 => { + result += 1; + } + _ => {} + } + } else { + runhistory.add_history(runy); + if !runcolor { + result += runhistory.count_patterns() * PENALTY_N3; + } + runcolor = self.module(x, y); + runy = 1; + } + } + result += runhistory.terminate_and_count(runcolor, runy) * PENALTY_N3; + } + + // 2*2 blocks of modules having same color + for y in 0..size - 1 { + for x in 0..size - 1 { + let color: bool = self.module(x, y); + if color == self.module(x + 1, y) + && color == self.module(x, y + 1) + && color == self.module(x + 1, y + 1) + { + result += PENALTY_N2; + } + } + } + + // Balance of dark and light modules + let dark: i32 = self.modules.iter().copied().map(i32::from).sum(); + let total: i32 = size * size; // Note that size is odd, so dark/total != 1/2 + // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)% + let k: i32 = ((dark * 20 - total * 10).abs() + total - 1) / total - 1; + debug_assert!((0..=9).contains(&k)); + result += k * PENALTY_N4; + debug_assert!((0..=2568888).contains(&result)); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4 + result + } + + /*---- Private helper functions ----*/ + + // Returns an ascending list of positions of alignment patterns for this version number. + // Each position is in the range [0,177), and are used on both the x and y axes. + // This could be implemented as lookup table of 40 variable-length lists of unsigned bytes. + fn get_alignment_pattern_positions(&self) -> Vec { + let ver: u8 = self.version.value(); + if ver == 1 { + vec![] + } else { + let numalign = i32::from(ver) / 7 + 2; + let step: i32 = if ver == 32 { + 26 + } else { + (i32::from(ver) * 4 + numalign * 2 + 1) / (numalign * 2 - 2) * 2 + }; + let mut result: Vec = (0..numalign - 1) + .map(|i| self.size - 7 - i * step) + .collect(); + result.push(6); + result.reverse(); + result + } + } + + // Returns the number of data bits that can be stored in a QR Code of the given version number, after + // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8. + // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table. + fn get_num_raw_data_modules(ver: Version) -> usize { + let ver = usize::from(ver.value()); + let mut result: usize = (16 * ver + 128) * ver + 64; + if ver >= 2 { + let numalign: usize = ver / 7 + 2; + result -= (25 * numalign - 10) * numalign - 55; + if ver >= 7 { + result -= 36; + } + } + debug_assert!((208..=29648).contains(&result)); + result + } + + // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any + // QR Code of the given version number and error correction level, with remainder bits discarded. + // This stateless pure function could be implemented as a (40*4)-cell lookup table. + fn get_num_data_codewords(ver: Version, ecl: QrCodeEcc) -> usize { + QrCode::get_num_raw_data_modules(ver) / 8 + - QrCode::table_get(&ECC_CODEWORDS_PER_BLOCK, ver, ecl) + * QrCode::table_get(&NUM_ERROR_CORRECTION_BLOCKS, ver, ecl) + } + + // Returns an entry from the given table based on the given values. + fn table_get(table: &'static [[i8; 41]; 4], ver: Version, ecl: QrCodeEcc) -> usize { + table[ecl.ordinal()][usize::from(ver.value())] as usize + } + + // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be + // implemented as a lookup table over all possible parameter values, instead of as an algorithm. + fn reed_solomon_compute_divisor(degree: usize) -> Vec { + assert!((1..=255).contains(°ree), "Degree out of range"); + // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1. + // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93]. + let mut result = vec![0u8; degree - 1]; + result.push(1); // Start off with the monomial x^0 + + // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), + // and drop the highest monomial term which is always 1x^degree. + // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). + let mut root: u8 = 1; + for _ in 0..degree { + // Unused variable i + // Multiply the current product by (x - r^i) + for j in 0..degree { + result[j] = QrCode::reed_solomon_multiply(result[j], root); + if j + 1 < result.len() { + result[j] ^= result[j + 1]; + } + } + root = QrCode::reed_solomon_multiply(root, 0x02); + } + result + } + + // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials. + fn reed_solomon_compute_remainder(data: &[u8], divisor: &[u8]) -> Vec { + let mut result = vec![0u8; divisor.len()]; + for b in data { + // Polynomial division + let factor: u8 = b ^ result.remove(0); + result.push(0); + for (x, &y) in result.iter_mut().zip(divisor.iter()) { + *x ^= QrCode::reed_solomon_multiply(y, factor); + } + } + result + } + + // Returns the product of the two given field elements modulo GF(2^8/0x11D). + // All inputs are valid. This could be implemented as a 256*256 lookup table. + fn reed_solomon_multiply(x: u8, y: u8) -> u8 { + // Russian peasant multiplication + let mut z: u8 = 0; + for i in (0..8).rev() { + z = (z << 1) ^ ((z >> 7) * 0x1D); + z ^= ((y >> i) & 1) * x; + } + z + } +} + +/*---- Helper struct for get_penalty_score() ----*/ + +struct FinderPenalty { + qr_size: i32, + run_history: [i32; 7], +} + +impl FinderPenalty { + pub fn new(size: i32) -> Self { + Self { + qr_size: size, + run_history: [0i32; 7], + } + } + + // Pushes the given value to the front and drops the last value. + pub fn add_history(&mut self, mut currentrunlength: i32) { + if self.run_history[0] == 0 { + currentrunlength += self.qr_size; // Add light border to initial run + } + let rh = &mut self.run_history; + for i in (0..rh.len() - 1).rev() { + rh[i + 1] = rh[i]; + } + rh[0] = currentrunlength; + } + + // Can only be called immediately after a light run is added, and returns either 0, 1, or 2. + pub fn count_patterns(&self) -> i32 { + let rh = &self.run_history; + let n = rh[1]; + debug_assert!(n <= self.qr_size * 3); + let core = n > 0 && rh[2] == n && rh[3] == n * 3 && rh[4] == n && rh[5] == n; + i32::from(core && rh[0] >= n * 4 && rh[6] >= n) + + i32::from(core && rh[6] >= n * 4 && rh[0] >= n) + } + + // Must be called at the end of a line (row or column) of modules. + pub fn terminate_and_count(mut self, currentruncolor: bool, mut currentrunlength: i32) -> i32 { + if currentruncolor { + // Terminate dark run + self.add_history(currentrunlength); + currentrunlength = 0; + } + currentrunlength += self.qr_size; // Add light border to final run + self.add_history(currentrunlength); + self.count_patterns() + } +} + +/*---- Constants and tables ----*/ + +// For use in get_penalty_score(), when evaluating which mask is best. +const PENALTY_N1: i32 = 3; +const PENALTY_N2: i32 = 3; +const PENALTY_N3: i32 = 40; +const PENALTY_N4: i32 = 10; + +static ECC_CODEWORDS_PER_BLOCK: [[i8; 41]; 4] = [ + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 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 Error correction level + [ + -1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, + 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + ], // Low + [ + -1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + ], // Medium + [ + -1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, + 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + ], // Quartile + [ + -1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + ], // High +]; + +static NUM_ERROR_CORRECTION_BLOCKS: [[i8; 41]; 4] = [ + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 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 Error correction level + [ + -1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, + 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25, + ], // Low + [ + -1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, + 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49, + ], // Medium + [ + -1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, + 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68, + ], // Quartile + [ + -1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, + 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81, + ], // High +]; + +/*---- QrCodeEcc functionality ----*/ + +/// The error correction level in a QR Code symbol. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum QrCodeEcc { + /// The QR Code can tolerate about 7% erroneous codewords. + Low, + /// The QR Code can tolerate about 15% erroneous codewords. + Medium, + /// The QR Code can tolerate about 25% erroneous codewords. + Quartile, + /// The QR Code can tolerate about 30% erroneous codewords. + High, +} + +impl QrCodeEcc { + // Returns an unsigned 2-bit integer (in the range 0 to 3). + fn ordinal(self) -> usize { + use QrCodeEcc::*; + match self { + Low => 0, + Medium => 1, + Quartile => 2, + High => 3, + } + } + + // Returns an unsigned 2-bit integer (in the range 0 to 3). + fn format_bits(self) -> u8 { + use QrCodeEcc::*; + match self { + Low => 1, + Medium => 0, + Quartile => 3, + High => 2, + } + } +} + +/*---- QrSegment functionality ----*/ + +/// A segment of character/binary/control data in a QR Code symbol. +/// +/// Instances of this struct are immutable. +/// +/// The mid-level way to create a segment is to take the payload data +/// and call a static factory function such as `QrSegment::make_numeric()`. +/// The low-level way to create a segment is to custom-make the bit buffer +/// and call the `QrSegment::new()` constructor with appropriate values. +/// +/// This segment struct imposes no length restrictions, but QR Codes have restrictions. +/// Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. +/// Any segment longer than this is meaningless for the purpose of generating QR Codes. +#[derive(Clone, PartialEq, Eq)] +pub struct QrSegment { + // The mode indicator of this segment. Accessed through mode(). + mode: QrSegmentMode, + + // The length of this segment's unencoded data. Measured in characters for + // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. + // Not the same as the data's bit length. Accessed through num_chars(). + numchars: usize, + + // The data bits of this segment. Accessed through data(). + data: Vec, +} + +impl QrSegment { + /*---- Static factory functions (mid level) ----*/ + + /// Returns a segment representing the given binary data encoded in byte mode. + /// + /// All input byte slices are acceptable. + /// + /// Any text string can be converted to UTF-8 bytes and encoded as a byte mode segment. + pub fn make_bytes(data: &[u8]) -> Self { + let mut bb = BitBuffer(Vec::with_capacity(data.len() * 8)); + for &b in data { + bb.append_bits(u32::from(b), 8); + } + QrSegment::new(QrSegmentMode::Byte, data.len(), bb.0) + } + + /// Returns a segment representing the given string of decimal digits encoded in numeric mode. + /// + /// Panics if the string contains non-digit characters. + pub fn make_numeric(text: &str) -> Self { + let mut bb = BitBuffer(Vec::with_capacity(text.len() * 3 + (text.len() + 2) / 3)); + let mut accumdata: u32 = 0; + let mut accumcount: u8 = 0; + for b in text.bytes() { + assert!( + (b'0'..=b'9').contains(&b), + "String contains non-numeric characters" + ); + accumdata = accumdata * 10 + u32::from(b - b'0'); + accumcount += 1; + if accumcount == 3 { + bb.append_bits(accumdata, 10); + accumdata = 0; + accumcount = 0; + } + } + if accumcount > 0 { + // 1 or 2 digits remaining + bb.append_bits(accumdata, accumcount * 3 + 1); + } + QrSegment::new(QrSegmentMode::Numeric, text.len(), bb.0) + } + + /// Returns a segment representing the given text string encoded in alphanumeric mode. + /// + /// The characters allowed are: 0 to 9, A to Z (uppercase only), space, + /// dollar, percent, asterisk, plus, hyphen, period, slash, colon. + /// + /// Panics if the string contains non-encodable characters. + pub fn make_alphanumeric(text: &str) -> Self { + let mut bb = BitBuffer(Vec::with_capacity(text.len() * 5 + (text.len() + 1) / 2)); + let mut accumdata: u32 = 0; + let mut accumcount: u32 = 0; + for c in text.chars() { + let i: usize = ALPHANUMERIC_CHARSET + .find(c) + .expect("String contains unencodable characters in alphanumeric mode"); + accumdata = accumdata * 45 + u32::try_from(i).unwrap(); + accumcount += 1; + if accumcount == 2 { + bb.append_bits(accumdata, 11); + accumdata = 0; + accumcount = 0; + } + } + if accumcount > 0 { + // 1 character remaining + bb.append_bits(accumdata, 6); + } + QrSegment::new(QrSegmentMode::Alphanumeric, text.len(), bb.0) + } + + /// Returns a list of zero or more segments to represent the given Unicode text string. + /// + /// The result may use various segment modes and switch + /// modes to optimize the length of the bit stream. + pub fn make_segments(text: &str) -> Vec { + if text.is_empty() { + vec![] + } else { + vec![if QrSegment::is_numeric(text) { + QrSegment::make_numeric(text) + } else if QrSegment::is_alphanumeric(text) { + QrSegment::make_alphanumeric(text) + } else { + QrSegment::make_bytes(text.as_bytes()) + }] + } + } + + /// Returns a segment representing an Extended Channel Interpretation + /// (ECI) designator with the given assignment value. + pub fn make_eci(assignval: u32) -> Self { + let mut bb = BitBuffer(Vec::with_capacity(24)); + if assignval < (1 << 7) { + bb.append_bits(assignval, 8); + } else if assignval < (1 << 14) { + bb.append_bits(0b10, 2); + bb.append_bits(assignval, 14); + } else if assignval < 1_000_000 { + bb.append_bits(0b110, 3); + bb.append_bits(assignval, 21); + } else { + panic!("ECI assignment value out of range"); + } + QrSegment::new(QrSegmentMode::Eci, 0, bb.0) + } + + /*---- Constructor (low level) ----*/ + + /// Creates a new QR Code segment with the given attributes and data. + /// + /// The character count (numchars) must agree with the mode and + /// the bit buffer length, but the constraint isn't checked. + pub fn new(mode: QrSegmentMode, numchars: usize, data: Vec) -> Self { + Self { + mode, + numchars, + data, + } + } + + /*---- Instance field getters ----*/ + + /// Returns the mode indicator of this segment. + pub fn mode(&self) -> QrSegmentMode { + self.mode + } + + /// Returns the character count field of this segment. + pub fn num_chars(&self) -> usize { + self.numchars + } + + /// Returns the data bits of this segment. + pub fn data(&self) -> &Vec { + &self.data + } + + /*---- Other static functions ----*/ + + // Calculates and returns the number of bits needed to encode the given + // segments at the given version. The result is None if a segment has too many + // characters to fit its length field, or the total bits exceeds usize::MAX. + fn get_total_bits(segs: &[Self], version: Version) -> Option { + let mut result: usize = 0; + for seg in segs { + let ccbits: u8 = seg.mode.num_char_count_bits(version); + // ccbits can be as large as 16, but usize can be as small as 16 + if let Some(limit) = 1usize.checked_shl(ccbits.into()) { + if seg.numchars >= limit { + return None; // The segment's length doesn't fit the field's bit width + } + } + result = result.checked_add(4 + usize::from(ccbits))?; + result = result.checked_add(seg.data.len())?; + } + Some(result) + } + + /// Tests whether the given string can be encoded as a segment in numeric mode. + /// + /// A string is encodable iff each character is in the range 0 to 9. + pub fn is_numeric(text: &str) -> bool { + text.chars().all(|c| ('0'..='9').contains(&c)) + } + + /// Tests whether the given string can be encoded as a segment in alphanumeric mode. + /// + /// A string is encodable iff each character is in the following set: 0 to 9, A to Z + /// (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. + pub fn is_alphanumeric(text: &str) -> bool { + text.chars().all(|c| ALPHANUMERIC_CHARSET.contains(c)) + } +} + +// The set of all legal characters in alphanumeric mode, +// where each character value maps to the index in the string. +static ALPHANUMERIC_CHARSET: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; + +/*---- QrSegmentMode functionality ----*/ + +/// Describes how a segment's data bits are interpreted. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum QrSegmentMode { + Numeric, + Alphanumeric, + Byte, + Kanji, + Eci, +} + +impl QrSegmentMode { + // Returns an unsigned 4-bit integer value (range 0 to 15) + // representing the mode indicator bits for this mode object. + pub fn mode_bits(self) -> u32 { + use QrSegmentMode::*; + match self { + Numeric => 0x1, + Alphanumeric => 0x2, + Byte => 0x4, + Kanji => 0x8, + Eci => 0x7, + } + } + + // Returns the bit width of the character count field for a segment in this mode + // in a QR Code at the given version number. The result is in the range [0, 16]. + pub fn num_char_count_bits(self, ver: Version) -> u8 { + use QrSegmentMode::*; + (match self { + Numeric => [10, 12, 14], + Alphanumeric => [9, 11, 13], + Byte => [8, 16, 16], + Kanji => [8, 10, 12], + Eci => [0, 0, 0], + })[usize::from((ver.value() + 7) / 17)] + } +} + +/*---- Bit buffer functionality ----*/ + +/// An appendable sequence of bits (0s and 1s). +/// +/// Mainly used by QrSegment. +pub struct BitBuffer(pub Vec); + +impl BitBuffer { + /// Appends the given number of low-order bits of the given value to this buffer. + /// + /// Requires len ≤ 31 and val < 2len. + pub fn append_bits(&mut self, val: u32, len: u8) { + assert!(len <= 31 && val >> len == 0, "Value out of range"); + self.0 + .extend((0..i32::from(len)).rev().map(|i| get_bit(val, i))); // Append bit by bit + } +} + +/*---- Miscellaneous values ----*/ + +/// The error type when the supplied data does not fit any QR Code version. +/// +/// Ways to handle this exception include: +/// +/// - Decrease the error correction level if it was greater than `QrCodeEcc::Low`. +/// - If the `encode_segments_advanced()` function was called, then increase the maxversion +/// argument if it was less than `Version::MAX`. (This advice does not apply to the +/// other factory functions because they search all versions up to `Version::MAX`.) +/// - Split the text data into better or optimal segments in order to reduce the number of bits required. +/// - Change the text or binary data to be shorter. +/// - Change the text to fit the character set of a particular segment mode (e.g. alphanumeric). +/// - Propagate the error upward to the caller/user. +#[derive(Debug, Clone)] +pub enum DataTooLong { + SegmentTooLong, + DataOverCapacity(usize, usize), +} + +impl std::error::Error for DataTooLong {} + +impl std::fmt::Display for DataTooLong { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + Self::SegmentTooLong => write!(f, "Segment too long"), + Self::DataOverCapacity(datalen, maxcapacity) => write!( + f, + "Data length = {} bits, Max capacity = {} bits", + datalen, maxcapacity + ), + } + } +} + +/// A number between 1 and 40 (inclusive). +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct Version(u8); + +impl Version { + /// The minimum version number supported in the QR Code Model 2 standard. + pub const MIN: Version = Version(1); + + /// The maximum version number supported in the QR Code Model 2 standard. + pub const MAX: Version = Version(40); + + /// Creates a version object from the given number. + /// + /// Panics if the number is outside the range [1, 40]. + pub fn new(ver: u8) -> Self { + assert!( + (Version::MIN.value()..=Version::MAX.value()).contains(&ver), + "Version number out of range" + ); + Self(ver) + } + + /// Returns the value, which is in the range [1, 40]. + pub fn value(self) -> u8 { + self.0 + } +} + +/// A number between 0 and 7 (inclusive). +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct Mask(u8); + +impl Mask { + /// Creates a mask object from the given number. + /// + /// Panics if the number is outside the range [0, 7]. + pub fn new(mask: u8) -> Self { + assert!(mask <= 7, "Mask value out of range"); + Self(mask) + } + + /// Returns the value, which is in the range [0, 7]. + pub fn value(self) -> u8 { + self.0 + } +} + +// Returns true iff the i'th bit of x is set to 1. +fn get_bit(x: u32, i: i32) -> bool { + (x >> i) & 1 != 0 +} + +// Prints the given QrCode object to the console. +pub fn print_qr(text: &str) { + let errcorlvl: QrCodeEcc = QrCodeEcc::Medium; // Error correction level + + // Make and print the QR Code symbol + let qr: QrCode = QrCode::encode_text(text, errcorlvl).unwrap(); + + let border: i32 = 4; + for y in -border / 2..qr.size() / 2 + border / 2 + 1 { + for x in -border..qr.size() + border { + let c = match (!qr.get_module(x, y * 2), !qr.get_module(x, y * 2 + 1)) { + (true, true) => '█', + (true, false) => '▀', + (false, true) => '▄', + (false, false) => ' ', + }; + print!("{0}", c); + } + println!(); + } + println!(); +} diff --git a/src/main.rs b/src/main.rs index ff6e6f7f..8e76cea4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,10 @@ pub struct CliOpts { #[clap(long)] seed_file: Option, + /// Output the result(s) as UTF-8 QR codes. + #[clap(long)] + qr: bool, + #[clap(subcommand)] command: commands::Command, } @@ -34,7 +38,7 @@ fn main() { ) }), }; - if let Err(err) = commands::exec(&pem, command) { + if let Err(err) = commands::exec(&pem, opts.qr, command) { eprintln!("{}", err); std::process::exit(1); } From 44df76049a71cfe91836d150e748c1ddfde33b05 Mon Sep 17 00:00:00 2001 From: John Wiegley Date: Wed, 26 Jan 2022 08:52:42 -0800 Subject: [PATCH 4/5] Add a get-neuron-info query command (#94) --- candid/governance.did | 1 + src/commands/get_neuron_info.rs | 27 +++++++++++++++++++++++++++ src/commands/mod.rs | 5 +++++ tests/commands/get-neuron-info.sh | 1 + tests/outputs/get-neuron-info.txt | 7 +++++++ 5 files changed, 41 insertions(+) create mode 100644 src/commands/get_neuron_info.rs create mode 100755 tests/commands/get-neuron-info.sh create mode 100644 tests/outputs/get-neuron-info.txt diff --git a/candid/governance.did b/candid/governance.did index 350c7773..ba459c7e 100644 --- a/candid/governance.did +++ b/candid/governance.did @@ -174,6 +174,7 @@ type NeuronInfo = record { recent_ballots : vec BallotInfo; created_timestamp_seconds : nat64; state : int32; + stake_e8s : nat64; retrieved_at_timestamp_seconds : nat64; voting_power : nat64; age_seconds : nat64; diff --git a/src/commands/get_neuron_info.rs b/src/commands/get_neuron_info.rs new file mode 100644 index 00000000..014a5400 --- /dev/null +++ b/src/commands/get_neuron_info.rs @@ -0,0 +1,27 @@ +use crate::{ + commands::send::submit_unsigned_ingress, + lib::{governance_canister_id, AnyhowResult}, +}; +use candid::Encode; +use clap::Parser; + +#[derive(Parser)] +pub struct GetNeuronInfoOpts { + pub ident: u64, + + /// Will display the query, but not send it. + #[clap(long)] + dry_run: bool, +} + +// We currently only support a subset of the functionality. +pub async fn exec(opts: GetNeuronInfoOpts) -> AnyhowResult { + let args = Encode!(&opts.ident)?; + submit_unsigned_ingress( + governance_canister_id(), + "get_neuron_info", + args, + opts.dry_run, + ) + .await +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 50126cab..c74a08b8 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -8,6 +8,7 @@ use tokio::runtime::Runtime; mod account_balance; mod claim_neurons; mod generate; +mod get_neuron_info; mod get_proposal_info; mod list_neurons; mod list_proposals; @@ -35,6 +36,7 @@ pub enum Command { ListNeurons(list_neurons::ListNeuronsOpts), ListProposals(list_proposals::ListProposalsOpts), GetProposalInfo(get_proposal_info::GetProposalInfoOpts), + GetNeuronInfo(get_neuron_info::GetNeuronInfoOpts), /// Queries a ledger account balance. AccountBalance(account_balance::AccountBalanceOpts), /// Generate a mnemonic seed phrase and generate or recover PEM. @@ -75,6 +77,9 @@ pub fn exec(pem: &Option, qr: bool, cmd: Command) -> AnyhowResult { Command::GetProposalInfo(opts) => { runtime.block_on(async { get_proposal_info::exec(opts).await }) } + Command::GetNeuronInfo(opts) => { + runtime.block_on(async { get_neuron_info::exec(opts).await }) + } Command::AccountBalance(opts) => { runtime.block_on(async { account_balance::exec(opts).await }) } diff --git a/tests/commands/get-neuron-info.sh b/tests/commands/get-neuron-info.sh new file mode 100755 index 00000000..2620fe9e --- /dev/null +++ b/tests/commands/get-neuron-info.sh @@ -0,0 +1 @@ +${CARGO_TARGET_DIR:-../target}/debug/quill get-neuron-info 22174 --dry-run diff --git a/tests/outputs/get-neuron-info.txt b/tests/outputs/get-neuron-info.txt new file mode 100644 index 00000000..69d34cb1 --- /dev/null +++ b/tests/outputs/get-neuron-info.txt @@ -0,0 +1,7 @@ +Sending message with + + Call type: query + Sender: 2vxsx-fae + Canister id: rrkah-fqaaa-aaaaa-aaaaq-cai + Method name: get_neuron_info + Arguments: (22_174 : nat64) From 310d7d659d85f520fe624cff54765bed4d986782 Mon Sep 17 00:00:00 2001 From: MarioDfinity <93518022+MarioDfinity@users.noreply.github.com> Date: Mon, 31 Jan 2022 10:03:34 +0100 Subject: [PATCH 5/5] TOB-DQK-007 remove panics (#95) --- Cargo.lock | 20 ++++-- Cargo.toml | 3 + default.nix | 2 +- src/commands/generate.rs | 6 +- src/commands/mod.rs | 3 +- src/commands/neuron_manage.rs | 14 ++-- src/commands/public.rs | 6 +- src/commands/request_status.rs | 37 ++++++---- src/commands/send.rs | 4 +- src/commands/transfer.rs | 27 ++++--- src/lib/mod.rs | 32 +++++---- src/lib/signing.rs | 6 +- src/main.rs | 127 ++++++++++++++++++++++++++------- 13 files changed, 198 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6cbfcdcf..30ec678a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -654,6 +654,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "features" version = "0.10.0" @@ -2135,6 +2144,7 @@ dependencies = [ "serde_cbor", "serde_json", "simple_asn1", + "tempfile", "tiny-hderive", "tokio", ] @@ -2346,9 +2356,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -2801,13 +2811,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if 1.0.0", + "fastrand", "libc", - "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi", diff --git a/Cargo.toml b/Cargo.toml index eee111d3..9dd3d04d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,5 +35,8 @@ serde_json = "1.0.57" tiny-hderive = "0.3.0" tokio = { version = "1.2.0", features = [ "fs" ] } +[dev-dependencies] +tempfile = "3.3.0" + [features] static-ssl = ["openssl/vendored"] diff --git a/default.nix b/default.nix index f92fa57e..beecb01e 100644 --- a/default.nix +++ b/default.nix @@ -14,7 +14,7 @@ with pkgs; rustPlatform.buildRustPackage rec { src = ./.; - cargoSha256 = "sha256-zFRMDnSNOVDCh+cupe7ZieR5UPrwHDZ9oi7MnzWpk2s="; + cargoSha256 = "sha256-t+N0UoVOJIxi1q0SBfCqxNIcmXVBo44hhKE0SllJ63M="; cargoBuildFlags = []; diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 97195f31..ddfbdca6 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -1,5 +1,5 @@ use crate::lib::{mnemonic_to_pem, AnyhowResult}; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use bip39::{Language, Mnemonic}; use clap::Parser; use rand::{rngs::OsRng, RngCore}; @@ -49,14 +49,14 @@ pub fn exec(opts: GenerateOpts) -> AnyhowResult { _ => return Err(anyhow!("Words must be 12 or 24.")), }; let mnemonic = match opts.phrase { - Some(phrase) => Mnemonic::parse(phrase).unwrap(), + Some(phrase) => Mnemonic::parse(phrase).context("Failed to parse mnemonic")?, None => { let mut key = vec![0u8; bytes]; OsRng.fill_bytes(&mut key); Mnemonic::from_entropy_in(Language::English, &key).unwrap() } }; - let pem = mnemonic_to_pem(&mnemonic); + let pem = mnemonic_to_pem(&mnemonic).context("Failed to convert mnemonic to PEM")?; let mut phrase = mnemonic .word_iter() .collect::>() diff --git a/src/commands/mod.rs b/src/commands/mod.rs index c74a08b8..0bbe1ae4 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,6 +1,7 @@ //! This module implements the command-line API. use crate::lib::{qr, require_pem, AnyhowResult}; +use anyhow::Context; use clap::Parser; use std::io::{self, Write}; use tokio::runtime::Runtime; @@ -159,7 +160,7 @@ where print(arg) } else { for (i, a) in arg.iter().enumerate() { - print_qr(&a, i != arg.len() - 1).expect("print_qr"); + print_qr(&a, i != arg.len() - 1).context("Failed to print QR code")?; } Ok(()) } diff --git a/src/commands/neuron_manage.rs b/src/commands/neuron_manage.rs index ef895218..4c4f5cb0 100644 --- a/src/commands/neuron_manage.rs +++ b/src/commands/neuron_manage.rs @@ -3,7 +3,7 @@ use crate::lib::{ signing::{sign_ingress_with_request_status_query, IngressWithRequestId}, AnyhowResult, }; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use candid::{CandidType, Encode}; use clap::Parser; use ic_types::Principal; @@ -166,7 +166,7 @@ pub fn exec(pem: &str, opts: ManageOpts) -> AnyhowResult AnyhowResult ONE_YEAR_SECONDS * 7, "EIGHT_YEARS" => ONE_YEAR_SECONDS * 8, - s => s.parse::().expect("Couldn't parse the dissolve delay"), + s => s + .parse::() + .context("Failed to parse the dissolve delay")?, } })) })), @@ -298,7 +300,7 @@ pub fn exec(pem: &str, opts: ManageOpts) -> AnyhowResult AnyhowResult u64 { +fn parse_neuron_id(id: String) -> AnyhowResult { id.replace("_", "") .parse() - .expect("Couldn't parse the neuron id") + .context("Failed to parse the neuron id") } diff --git a/src/commands/public.rs b/src/commands/public.rs index 25ca3a8f..f456573f 100644 --- a/src/commands/public.rs +++ b/src/commands/public.rs @@ -35,9 +35,7 @@ fn get_public_ids( /// Returns the account id and the principal id if the private key was provided. pub fn get_ids(pem: &Option) -> AnyhowResult<(Principal, AccountIdentifier)> { - require_pem(pem)?; - let principal_id = get_identity(pem.as_ref().unwrap()) - .sender() - .map_err(|e| anyhow!(e))?; + let pem = require_pem(pem)?; + let principal_id = get_identity(&pem).sender().map_err(|e| anyhow!(e))?; Ok((principal_id, get_account_id(principal_id)?)) } diff --git a/src/commands/request_status.rs b/src/commands/request_status.rs index 23a802c9..cbcf92f0 100644 --- a/src/commands/request_status.rs +++ b/src/commands/request_status.rs @@ -1,22 +1,25 @@ use crate::lib::get_ic_url; use crate::lib::{get_agent, get_idl_string, signing::RequestStatus, AnyhowResult}; use anyhow::{anyhow, Context}; -use ic_agent::agent::{Replied, RequestStatusResponse}; +use ic_agent::agent::{ReplicaV2Transport, Replied, RequestStatusResponse}; +use ic_agent::AgentError::MessageError; use ic_agent::{AgentError, RequestId}; use ic_types::Principal; +use std::future::Future; +use std::pin::Pin; use std::str::FromStr; use std::sync::Arc; pub async fn submit(req: &RequestStatus, method_name: Option) -> AnyhowResult { - let canister_id = Principal::from_text(&req.canister_id).expect("Couldn't parse canister id"); - let request_id = - RequestId::from_str(&req.request_id).context("Invalid argument: request_id")?; + let canister_id = + Principal::from_text(&req.canister_id).context("Cannot parse the canister id")?; + let request_id = RequestId::from_str(&req.request_id).context("Cannot parse the request_id")?; let mut agent = get_agent("")?; agent.set_transport(ProxySignReplicaV2Transport { req: req.clone(), http_transport: Arc::new( ic_agent::agent::http_transport::ReqwestHttpReplicaV2Transport::create(get_ic_url()) - .unwrap(), + .context("Failed to create an agent")?, ), }); let Replied::CallReplied(blob) = async { @@ -57,20 +60,28 @@ pub(crate) struct ProxySignReplicaV2Transport { http_transport: Arc, } -use ic_agent::agent::ReplicaV2Transport; -use std::future::Future; -use std::pin::Pin; - impl ReplicaV2Transport for ProxySignReplicaV2Transport { fn read_state<'a>( &'a self, _canister_id: Principal, _content: Vec, ) -> Pin, AgentError>> + Send + 'a>> { - self.http_transport.read_state( - Principal::from_text(self.req.canister_id.clone()).unwrap(), - hex::decode(self.req.content.clone()).unwrap(), - ) + async fn run(transport: &ProxySignReplicaV2Transport) -> Result, AgentError> { + let canister_id = Principal::from_text(transport.req.canister_id.clone()) + .map_err(|err| MessageError(format!("Unable to parse canister_id: {}", err)))?; + let envelope = hex::decode(transport.req.content.clone()).map_err(|err| { + MessageError(format!( + "Unable to decode request content (should be hexadecimal encoded): {}", + err + )) + })?; + transport + .http_transport + .read_state(canister_id, envelope) + .await + } + + Box::pin(run(self)) } fn call<'a>( diff --git a/src/commands/send.rs b/src/commands/send.rs index 358f80c4..d3feb056 100644 --- a/src/commands/send.rs +++ b/src/commands/send.rs @@ -4,7 +4,7 @@ use crate::lib::{ signing::{Ingress, IngressWithRequestId}, AnyhowResult, }; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use candid::CandidType; use clap::Parser; use ic_agent::agent::ReplicaV2Transport; @@ -153,7 +153,7 @@ async fn send(message: &Ingress, opts: &SendOpts) -> AnyhowResult { &message .clone() .request_id - .expect("Cannot get request_id from the update message"), + .context("Cannot get request_id from the update message")?, )?; transport.call(canister_id, content, request_id).await?; let request_id = format!("0x{}", String::from(request_id)); diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 92bd8d81..3316d713 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -4,7 +4,7 @@ use crate::lib::{ signing::{sign_ingress_with_request_status_query, IngressWithRequestId}, AnyhowResult, }; -use anyhow::anyhow; +use anyhow::{anyhow, bail, Context}; use candid::Encode; use clap::Parser; use ledger_canister::{ICPTs, TRANSACTION_FEE}; @@ -29,16 +29,15 @@ pub struct TransferOpts { } pub fn exec(pem: &str, opts: TransferOpts) -> AnyhowResult> { - let amount = - parse_icpts(&opts.amount).map_err(|err| anyhow!("Could not add ICPs and e8s: {}", err))?; + let amount = parse_icpts(&opts.amount).context("Cannot parse amount")?; let fee = opts.fee.map_or(Ok(TRANSACTION_FEE), |v| { - parse_icpts(&v).map_err(|err| anyhow!(err)) + parse_icpts(&v).context("Cannot parse fee") })?; let memo = Memo( opts.memo .unwrap_or_else(|| "0".to_string()) .parse::() - .unwrap(), + .context("Failed to parse memo as unsigned integer")?, ); let to = opts.to; @@ -55,26 +54,32 @@ pub fn exec(pem: &str, opts: TransferOpts) -> AnyhowResult Result { +fn new_icps(icpt: u64, e8s: u64) -> AnyhowResult { + ICPTs::new(icpt, e8s) + .map_err(|err| anyhow!(err)) + .context("Cannot create new ICPs") +} + +fn parse_icpts(amount: &str) -> AnyhowResult { let parse = |s: &str| { s.parse::() - .map_err(|err| format!("Couldn't parse as u64: {:?}", err)) + .context("Failed to parse ICPTs as unsigned integer") }; match &amount.split('.').collect::>().as_slice() { - [icpts] => ICPTs::new(parse(icpts)?, 0), + [icpts] => new_icps(parse(icpts)?, 0), [icpts, e8s] => { let mut e8s = e8s.to_string(); while e8s.len() < 8 { e8s.push('0'); } let e8s = &e8s[..8]; - ICPTs::new(parse(icpts)?, parse(e8s)?) + new_icps(parse(icpts)?, parse(e8s)?) } - _ => Err(format!("Can't parse amount {}", amount)), + _ => bail!("Cannot parse amount {}", amount), } } -fn icpts_amount_validator(icpts: &str) -> Result<(), String> { +fn icpts_amount_validator(icpts: &str) -> AnyhowResult<()> { parse_icpts(icpts).map(|_| ()) } diff --git a/src/lib/mod.rs b/src/lib/mod.rs index fff06b5f..85adda13 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -1,6 +1,6 @@ //! All the common functionality. -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use bip39::Mnemonic; use candid::{ parser::typing::{check_prog, TypeEnv}, @@ -49,12 +49,13 @@ pub fn genesis_token_canister_id() -> Principal { pub fn get_local_candid(canister_id: Principal) -> AnyhowResult { if canister_id == governance_canister_id() { String::from_utf8(include_bytes!("../../candid/governance.did").to_vec()) - .map_err(|e| anyhow!(e)) + .context("Cannot load governance.did") } else if canister_id == ledger_canister_id() { String::from_utf8(include_bytes!("../../candid/ledger.did").to_vec()) - .map_err(|e| anyhow!(e)) + .context("Cannot load ledger.did") } else if canister_id == genesis_token_canister_id() { - String::from_utf8(include_bytes!("../../candid/gtc.did").to_vec()).map_err(|e| anyhow!(e)) + String::from_utf8(include_bytes!("../../candid/gtc.did").to_vec()) + .context("Cannot load gtc.did") } else { unreachable!() } @@ -101,10 +102,9 @@ pub fn read_from_file(path: &str) -> AnyhowResult { std::io::stdin().read_to_string(&mut content)?; } else { let path = std::path::Path::new(&path); - let mut file = - std::fs::File::open(&path).map_err(|_| anyhow!("Message file doesn't exist"))?; + let mut file = std::fs::File::open(&path).context("Cannot open the message file.")?; file.read_to_string(&mut content) - .map_err(|_| anyhow!("Cannot read the message file."))?; + .context("Cannot read the message file.")?; } Ok(content) } @@ -158,7 +158,7 @@ pub fn parse_query_response( method_name: &str, ) -> AnyhowResult { let cbor: Value = serde_cbor::from_slice(&response) - .map_err(|_| anyhow!("Invalid cbor data in the content of the message."))?; + .context("Invalid cbor data in the content of the message.")?; if let Value::Map(m) = cbor { // Try to decode a rejected response. if let (_, Some(Value::Integer(reject_code)), Some(Value::Text(reject_message))) = ( @@ -196,8 +196,8 @@ pub fn get_account_id(principal_id: Principal) -> AnyhowResult String { - fn der_encode_secret_key(public_key: Vec, secret: Vec) -> Vec { +pub fn mnemonic_to_pem(mnemonic: &Mnemonic) -> AnyhowResult { + fn der_encode_secret_key(public_key: Vec, secret: Vec) -> AnyhowResult> { let secp256k1_id = ObjectIdentifier(0, oid!(1, 3, 132, 0, 10)); let data = Sequence( 0, @@ -218,18 +218,20 @@ pub fn mnemonic_to_pem(mnemonic: &Mnemonic) -> String { ), ], ); - to_der(&data).expect("Cannot encode secret key.") + to_der(&data).context("Failed to encode secp256k1 secret key to DER") } let seed = mnemonic.to_seed(""); - let ext = tiny_hderive::bip32::ExtendedPrivKey::derive(&seed, "m/44'/223'/0'/0/0").unwrap(); + let ext = tiny_hderive::bip32::ExtendedPrivKey::derive(&seed, "m/44'/223'/0'/0/0") + .map_err(|err| anyhow!("{:?}", err)) + .context("Failed to derive BIP32 extended private key")?; let secret = ext.secret(); - let secret_key = SecretKey::parse(&secret).unwrap(); + let secret_key = SecretKey::parse(&secret).context("Failed to parse secret key")?; let public_key = PublicKey::from_secret_key(&secret_key); - let der = der_encode_secret_key(public_key.serialize().to_vec(), secret.to_vec()); + let der = der_encode_secret_key(public_key.serialize().to_vec(), secret.to_vec())?; let pem = Pem { tag: String::from("EC PRIVATE KEY"), contents: der, }; - encode(&pem).replace("\r", "").replace("\n\n", "\n") + Ok(encode(&pem).replace("\r", "").replace("\n\n", "\n")) } diff --git a/src/lib/signing.rs b/src/lib/signing.rs index 91ca9e3b..2c90c637 100644 --- a/src/lib/signing.rs +++ b/src/lib/signing.rs @@ -1,7 +1,7 @@ use crate::lib::get_idl_string; use crate::lib::AnyhowResult; use crate::lib::{get_candid_type, get_local_candid}; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use ic_agent::agent::QueryBuilder; use ic_agent::agent::UpdateBuilder; use ic_agent::RequestId; @@ -53,7 +53,7 @@ pub struct IngressWithRequestId { impl Ingress { pub fn parse(&self) -> AnyhowResult<(Principal, Principal, String, String)> { let cbor: Value = serde_cbor::from_slice(&hex::decode(&self.content)?) - .map_err(|_| anyhow!("Invalid cbor data in the content of the message."))?; + .context("Invalid cbor data in the content of the message.")?; if let Value::Map(m) = cbor { let cbor_content = m .get(&Value::Text("content".to_string())) @@ -154,7 +154,7 @@ pub fn sign_ingress_with_request_status_query( let msg_with_req_id = sign(pem, canister_id, method_name, args)?; let request_id = msg_with_req_id .request_id - .expect("No request id for transfer call found"); + .context("No request id for transfer call found")?; let request_status = request_status_sign(pem, request_id, canister_id)?; let message = IngressWithRequestId { ingress: msg_with_req_id.message, diff --git a/src/main.rs b/src/main.rs index 8e76cea4..8b34f494 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,10 @@ #![warn(unused_extern_crates)] + +use crate::lib::AnyhowResult; +use anyhow::Context; use bip39::Mnemonic; use clap::{crate_version, Parser}; + mod commands; mod lib; @@ -26,39 +30,112 @@ pub struct CliOpts { fn main() { let opts = CliOpts::parse(); - let command = opts.command; - // Get PEM from the file if provided, or try to convert from the seed file - let pem = match opts.pem_file { - Some(path) => Some(read_file(path)), - None => opts.seed_file.map(|path| { - let phrase = read_file(path); - lib::mnemonic_to_pem( - &Mnemonic::parse(phrase) - .expect("Couldn't parse the seed phrase as a valid mnemonic"), - ) - }), - }; - if let Err(err) = commands::exec(&pem, opts.qr, command) { - eprintln!("{}", err); + if let Err(err) = run(opts) { + for (level, cause) in err.chain().enumerate() { + if level == 0 { + eprintln!("Error: {}", err); + continue; + } + if level == 1 { + eprintln!("Caused by:"); + } + eprintln!("{:width$}{}", "", cause, width = level * 2); + } std::process::exit(1); } } -fn read_file(path: String) -> String { - match path.as_str() { +fn run(opts: CliOpts) -> AnyhowResult<()> { + let pem = read_pem(opts.pem_file, opts.seed_file)?; + commands::exec(&pem, opts.qr, opts.command) +} + +// Get PEM from the file if provided, or try to convert from the seed file +fn read_pem(pem_file: Option, seed_file: Option) -> AnyhowResult> { + match (pem_file, seed_file) { + (Some(pem_file), _) => read_file(&pem_file, "PEM").map(Some), + (_, Some(seed_file)) => { + let seed = read_file(&seed_file, "seed")?; + let mnemonic = parse_mnemonic(&seed)?; + let mnemonic = lib::mnemonic_to_pem(&mnemonic)?; + Ok(Some(mnemonic)) + } + _ => Ok(None), + } +} + +fn parse_mnemonic(phrase: &str) -> AnyhowResult { + Mnemonic::parse(phrase).context("Couldn't parse the seed phrase as a valid mnemonic. {:?}") +} + +fn read_file(path: &str, name: &str) -> AnyhowResult { + match path { // read from STDIN "-" => { let mut buffer = String::new(); use std::io::Read; - if let Err(err) = std::io::stdin().read_to_string(&mut buffer) { - eprintln!("Couldn't read from STDIN: {:?}", err); - std::process::exit(1); - } - buffer + std::io::stdin() + .read_to_string(&mut buffer) + .map(|_| buffer) + .context(format!("Couldn't read {} from STDIN", name)) } - path => std::fs::read_to_string(path).unwrap_or_else(|err| { - eprintln!("Couldn't read PEM file: {:?}", err); - std::process::exit(1); - }), + path => std::fs::read_to_string(path).context(format!("Couldn't read {} file", name)), } } + +#[test] +fn test_read_pem_none_none() { + let res = read_pem(None, None); + assert_eq!(None, res.expect("read_pem(None, None) failed")); +} + +#[test] +fn test_read_pem_from_pem_file() { + use std::io::Write; + + let mut pem_file = tempfile::NamedTempFile::new().expect("Cannot create temp file"); + + let content = "pem".to_string(); + pem_file + .write_all(content.as_bytes()) + .expect("Cannot write to temp file"); + + let res = read_pem(Some(pem_file.path().to_str().unwrap().to_string()), None); + + assert_eq!(Some(content), res.expect("read_pem from pem file")); +} + +#[test] +fn test_read_pem_from_seed_file() { + use std::io::Write; + + let mut seed_file = tempfile::NamedTempFile::new().expect("Cannot create temp file"); + + let phrase = "ozone drill grab fiber curtain grace pudding thank cruise elder eight about"; + seed_file + .write_all(phrase.as_bytes()) + .expect("Cannot write to temp file"); + let mnemonic = lib::mnemonic_to_pem(&Mnemonic::parse(phrase).unwrap()).unwrap(); + + let pem = read_pem(None, Some(seed_file.path().to_str().unwrap().to_string())) + .expect("Unable to read seed_file") + .expect("None returned instead of Some"); + + assert_eq!(mnemonic, pem); +} + +#[test] +fn test_read_pem_from_non_existing_file() { + let dir = tempfile::tempdir().expect("Cannot create temp dir"); + let non_existing_file = dir + .path() + .join("non_existing_pem_file") + .as_path() + .to_str() + .unwrap() + .to_string(); + + read_pem(Some(non_existing_file.clone()), None).unwrap_err(); + + read_pem(None, Some(non_existing_file)).unwrap_err(); +}