diff --git a/.vscode/settings.json b/.vscode/settings.json index b458b995e1..f5738c794a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "rust-analyzer.diagnostics.disabled": [ "unresolved-macro-call" - ] + ], + "rust-analyzer.cargo.features": "all" } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0317df288a..de9341c490 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,9 +355,9 @@ dependencies = [ [[package]] name = "bip0039" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e68a5a99c65851e7be249f5cf510c0a136f18c9bca32139576d59bd3f577b043" +checksum = "568b6890865156d9043af490d4c4081c385dd68ea10acd6ca15733d511e6b51c" dependencies = [ "hmac", "pbkdf2", @@ -486,6 +486,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + [[package]] name = "byteorder" version = "1.5.0" @@ -884,6 +890,18 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "educe" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "either" version = "1.13.0" @@ -905,6 +923,26 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -1291,6 +1329,19 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hidapi" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "pkg-config", + "windows-sys 0.48.0", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1757,6 +1808,75 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "ledger-apdu" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c21ffd28d97c9252671ab2ebe7078c9fa860ff3c5a125039e174d25ec6872169" +dependencies = [ + "arrayref", + "no-std-compat", + "snafu", +] + +[[package]] +name = "ledger-transport" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f18de77d956a030dbc5869ced47d404bbd641216ef2f9dce7ca90833ca64ff" +dependencies = [ + "async-trait", + "ledger-apdu", +] + +[[package]] +name = "ledger-transport-hid" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e34341e2708fbf805a9ada44ef6182170c6464c4fc068ab801abb7562fd5e8" +dependencies = [ + "byteorder", + "cfg-if", + "hex", + "hidapi", + "ledger-transport", + "libc", + "log", + "thiserror", +] + +[[package]] +name = "ledger-zcash" +version = "2.0.0" +source = "git+https://github.com/zecdev/ledger-zcash-rs.git?rev=e441e4c0e1c21a77ed7bfaf768eabd6074df5124#e441e4c0e1c21a77ed7bfaf768eabd6074df5124" +dependencies = [ + "byteorder", + "cfg-if", + "educe", + "hex", + "lazy_static", + "ledger-transport", + "ledger-zondax-generic", + "log", + "serde", + "sha2", + "thiserror", + "tokio", + "zx-bip44", +] + +[[package]] +name = "ledger-zondax-generic" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b7e1b78719fff5e70a67bab7224b1ed917aebb2b38521ffa047a9d93cc0501" +dependencies = [ + "async-trait", + "ledger-transport", + "serde", + "thiserror", +] + [[package]] name = "libc" version = "0.2.167" @@ -1914,9 +2034,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memuse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2145869435ace5ea6ea3d35f59be559317ec9a0d04e1812d5f185a87b6d36f1a" +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" dependencies = [ "nonempty", ] @@ -1996,6 +2116,12 @@ dependencies = [ "libc", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -2218,9 +2344,9 @@ dependencies = [ [[package]] name = "password-hash" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core 0.6.4", @@ -2250,9 +2376,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pbkdf2" -version = "0.11.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest", "password-hash", @@ -3163,6 +3289,27 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "socket2" version = "0.5.8" @@ -4586,6 +4733,7 @@ dependencies = [ "bls12_381", "bs58", "build_utils", + "bytemuck", "byteorder", "bytes", "chrono", @@ -4602,6 +4750,10 @@ dependencies = [ "json", "jubjub", "lazy_static", + "ledger-transport", + "ledger-transport-hid", + "ledger-zcash", + "ledger-zondax-generic", "log", "log4rs", "nonempty", @@ -4610,6 +4762,7 @@ dependencies = [ "proptest", "prost", "rand 0.8.5", + "redjubjub", "reqwest", "ring", "rust-embed", @@ -4642,6 +4795,7 @@ dependencies = [ "zingo-status", "zingo-sync", "zip32", + "zx-bip44", ] [[package]] @@ -4667,3 +4821,13 @@ dependencies = [ "zcash_address", "zcash_protocol", ] + +[[package]] +name = "zx-bip44" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ed17b569d1ea6903d5dc061602c6dc45f816cd0171d67d3b40fc1f6caf1ade0" +dependencies = [ + "byteorder", + "thiserror", +] diff --git a/Cargo.toml b/Cargo.toml index 19b8c1cb63..f8f7b0cdaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ members = [ resolver = "2" [workspace.dependencies] -bip0039 = "0.11" +bip0039 = "0.12" incrementalmerkletree = "0.7" orchard = "0.10" sapling-crypto = "0.3" diff --git a/libtonode-tests/Cargo.toml b/libtonode-tests/Cargo.toml index 314855c7d7..5a72be8078 100644 --- a/libtonode-tests/Cargo.toml +++ b/libtonode-tests/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [features] chain_generic_tests = [] ci = ["zingolib/ci"] +ledger-support = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/libtonode-tests/tests/concrete.rs b/libtonode-tests/tests/concrete.rs index 1c2afcbaf3..0fa51bbb84 100644 --- a/libtonode-tests/tests/concrete.rs +++ b/libtonode-tests/tests/concrete.rs @@ -1601,6 +1601,8 @@ mod slow { Some(client_builder.zingo_datadir), ChainType::Regtest(regtest_network), true, + #[cfg(feature = "ledger-support")] + false, ) .unwrap(); diff --git a/libtonode-tests/tests/sync.rs b/libtonode-tests/tests/sync.rs index a0a8e196d8..9a0d2f40b6 100644 --- a/libtonode-tests/tests/sync.rs +++ b/libtonode-tests/tests/sync.rs @@ -26,6 +26,8 @@ async fn sync_mainnet_test() { Some(temp_path), zingolib::config::ChainType::Mainnet, true, + #[cfg(feature = "ledger-support")] + false, ) .unwrap(); let mut lightclient = LightClient::create_from_wallet_base_async( diff --git a/zingocli/Cargo.toml b/zingocli/Cargo.toml index a3af13608f..c7aba22860 100644 --- a/zingocli/Cargo.toml +++ b/zingocli/Cargo.toml @@ -15,3 +15,6 @@ rustyline = { workspace = true } shellwords = { workspace = true } rustls.workspace = true + +[features] +ledger-support = [] diff --git a/zingocli/src/lib.rs b/zingocli/src/lib.rs index d6d3b6c6fe..3114f80d35 100644 --- a/zingocli/src/lib.rs +++ b/zingocli/src/lib.rs @@ -20,60 +20,63 @@ pub mod version; /// TODO: Add Doc Comment Here! pub fn build_clap_app() -> clap::ArgMatches { - clap::Command::new("Zingo CLI").version(version::VERSION) - .arg(Arg::new("nosync") - .help("By default, zingo-cli will sync the wallet at startup. Pass --nosync to prevent the automatic sync at startup.") - .long("nosync") - .short('n') - .action(clap::ArgAction::SetTrue)) - .arg(Arg::new("regtest") - .long("regtest") - .help("Regtest mode") - .action(clap::ArgAction::SetTrue) ) - .arg(Arg::new("no-clean") - .long("no-clean") - .help("Don't clean regtest state before running. Regtest mode only") - .action(clap::ArgAction::SetTrue)) - .arg(Arg::new("chain") - .long("chain").short('c') - .help(r#"What chain to expect, if it's not inferable from the server URI. One of "mainnet", "testnet", or "regtest""#)) - .arg(Arg::new("seed") - .short('s') - .long("seed") - .value_name("SEED PHRASE") - .value_parser(parse_seed) - .help("Create a new wallet with the given 24-word seed phrase. Will fail if wallet already exists")) - .arg(Arg::new("viewkey") - .long("viewkey") - .value_name("UFVK") - .value_parser(parse_ufvk) - .help("Create a new wallet with the given encoded unified full viewing key. Will fail if wallet already exists")) - .arg(Arg::new("birthday") - .long("birthday") - .value_name("birthday") - .value_parser(clap::value_parser!(u32)) - .help("Specify wallet birthday when restoring from seed. This is the earliest block height where the wallet has a transaction.")) - .arg(Arg::new("server") - .long("server") - .value_name("server") - .help("Lightwalletd server to connect to.") - .value_parser(parse_uri) - .default_value(zingolib::config::DEFAULT_LIGHTWALLETD_SERVER)) - .arg(Arg::new("data-dir") - .long("data-dir") - .value_name("data-dir") - .help("Absolute path to use as data directory")) - .arg(Arg::new("COMMAND") - .help("Command to execute. If a command is not specified, zingo-cli will start in interactive mode.") - .required(false) - .index(1)) - .arg(Arg::new("extra_args") - .help("Params to execute command with. Run the 'help' command to get usage help.") - .required(false) - .num_args(1..) - .index(2) - .action(clap::ArgAction::Append) - ).get_matches() + let cmd = clap::Command::new("Zingo CLI").version(version::VERSION) + .arg(Arg::new("nosync") + .help("By default, zingo-cli will sync the wallet at startup. Pass --nosync to prevent the automatic sync at startup.") + .long("nosync") + .short('n') + .action(clap::ArgAction::SetTrue)) + .arg(Arg::new("regtest") + .long("regtest") + .help("Regtest mode") + .action(clap::ArgAction::SetTrue) ) + .arg(Arg::new("no-clean") + .long("no-clean") + .help("Don't clean regtest state before running. Regtest mode only") + .action(clap::ArgAction::SetTrue)) + .arg(Arg::new("chain") + .long("chain").short('c') + .help(r#"What chain to expect, if it's not inferable from the server URI. One of "mainnet", "testnet", or "regtest""#)) + .arg(Arg::new("seed") + .short('s') + .long("seed") + .value_name("SEED PHRASE") + .value_parser(parse_seed) + .help("Create a new wallet with the given 24-word seed phrase. Will fail if wallet already exists")) + .arg(Arg::new("viewkey") + .long("viewkey") + .value_name("UFVK") + .value_parser(parse_ufvk) + .help("Create a new wallet with the given encoded unified full viewing key. Will fail if wallet already exists")) + .arg(Arg::new("birthday") + .long("birthday") + .value_name("birthday") + .value_parser(clap::value_parser!(u32)) + .help("Specify wallet birthday when restoring from seed. This is the earliest block height where the wallet has a transaction.")) + .arg(Arg::new("server") + .long("server") + .value_name("server") + .help("Lightwalletd server to connect to.") + .value_parser(parse_uri) + .default_value(zingolib::config::DEFAULT_LIGHTWALLETD_SERVER)) + .arg(Arg::new("data-dir") + .long("data-dir") + .value_name("data-dir") + .help("Absolute path to use as data directory")) + .arg(Arg::new("COMMAND") + .help("Command to execute. If a command is not specified, zingo-cli will start in interactive mode.") + .required(false) + .index(1)) + .arg(Arg::new("extra_args") + .help("Params to execute command with. Run the 'help' command to get usage help.") + .required(false) + .num_args(1..) + .index(2) + .action(clap::ArgAction::Append) + ); + + add_if_ledger(cmd) + .get_matches() } /// Custom function to parse a string into an http::Uri @@ -280,6 +283,8 @@ pub struct ConfigTemplate { #[allow(dead_code)] // This field is defined so that it can be used in Drop::drop child_process_handler: Option, chaintype: ChainType, + #[cfg(feature = "ledger-support")] + ledger: bool, } use commands::ShortCircuitedCommand; fn short_circuit_on_help(params: Vec) { @@ -354,6 +359,10 @@ If you don't remember the block height, you can pass '--birthday 0' to scan from }; let clean_regtest_data = !matches.get_flag("no-clean"); + + #[cfg(feature = "ledger-support")] + let ledger = matches.get_flag("ledger"); + let data_dir = if let Some(dir) = matches.get_one::("data-dir") { PathBuf::from(dir.clone()) } else if is_regtest { @@ -412,6 +421,8 @@ If you don't remember the block height, you can pass '--birthday 0' to scan from regtest_manager, child_process_handler, chaintype, + #[cfg(feature = "ledger-support")] + ledger, }) } } @@ -439,6 +450,8 @@ pub fn startup( Some(data_dir), filled_template.chaintype, true, + #[cfg(feature = "ledger-support")] + filled_template.ledger, ) .unwrap(); regtest_config_check(&filled_template.regtest_manager, &config.chain); @@ -551,3 +564,18 @@ pub fn run_cli() { Err(e) => eprintln!("Error filling config template: {:?}", e), } } + + +fn add_if_ledger(cmd: clap::Command) -> clap::Command { + + #[cfg(feature = "ledger-support")] + return cmd + .arg(Arg::new("ledger") + .long("ledger") + .value_name("ledger") + .help("Create a new wallet by connecting to a ledger") + .action(clap::ArgAction::SetTrue)); + + #[cfg(not(feature = "ledger-support"))] + return cmd; +} \ No newline at end of file diff --git a/zingolib/Cargo.toml b/zingolib/Cargo.toml index 18d305068c..e5a7218de9 100644 --- a/zingolib/Cargo.toml +++ b/zingolib/Cargo.toml @@ -16,6 +16,13 @@ tempfile = ["dep:tempfile"] test-elevation = ["portpicker", "testvectors", "tempfile", "tempdir"] sync = ['dep:zingo-sync'] zaino-test = ['test-elevation'] +ledger-support = [ + "dep:ledger-zcash", + "dep:zx-bip44", + "dep:ledger-transport", + "dep:ledger-zondax-generic", + "dep:bytemuck", +] [dependencies] zingo-memo = { path = "../zingo-memo" } @@ -85,6 +92,17 @@ tonic = { workspace = true } tracing-subscriber = { workspace = true } bech32 = { workspace = true } +# Ledger Support +ledger-zcash = { git = "https://github.com/zecdev/ledger-zcash-rs.git", rev = "e441e4c0e1c21a77ed7bfaf768eabd6074df5124", optional = true } +zx-bip44 = { version = "0.1.0", optional = true } +redjubjub = { version = "0.7", optional = true } +ledger-transport = { version = "0.11", optional = true } +ledger-zondax-generic = { version = "0.11", optional = true } +ledger-transport-hid = { version = "0.11", optional = true } + +# Manage HidApi instance. +bytemuck = { version = "1.20.0", optional = true } + [dev-dependencies] portpicker = { workspace = true } tempfile = { workspace = true } diff --git a/zingolib/src/config.rs b/zingolib/src/config.rs index e1ec4455b1..59385c8d2a 100644 --- a/zingolib/src/config.rs +++ b/zingolib/src/config.rs @@ -69,6 +69,8 @@ pub fn load_clientconfig( data_dir: Option, chain: ChainType, monitor_mempool: bool, + #[cfg(feature = "ledger-support")] + ledger: bool, ) -> std::io::Result { use std::net::ToSocketAddrs; @@ -103,6 +105,8 @@ pub fn load_clientconfig( wallet_dir: data_dir, wallet_name: DEFAULT_WALLET_NAME.into(), logfile_name: DEFAULT_LOGFILE_NAME.into(), + #[cfg(feature = "ledger-support")] + use_ledger: ledger, }; Ok(config) @@ -151,6 +155,9 @@ pub struct ZingoConfigBuilder { pub wallet_name: Option, /// The filename of the logfile. This will be created in the `wallet_dir`. pub logfile_name: Option, + /// set to true if you will use a ledger hw wallet + #[cfg(feature = "ledger-support")] + pub use_ledger: bool, } /// Configuration data that is necessary? and sufficient? for the creation of a LightClient. @@ -170,6 +177,9 @@ pub struct ZingoConfig { pub wallet_name: PathBuf, /// The filename of the logfile. This will be created in the `wallet_dir`. pub logfile_name: PathBuf, + #[cfg(feature = "ledger-support")] + /// if this option is enabled, the LightClient will look for a ledger device to initialize or resume the wallet + pub use_ledger: bool, } impl ZingoConfigBuilder { @@ -214,6 +224,13 @@ impl ZingoConfigBuilder { self } + #[cfg(feature = "ledger-support")] + /// set to true to use a ledger hardware wallet + pub fn set_use_ledger(&mut self, ledger: bool) -> &mut Self { + self.use_ledger = ledger; + self + } + /// TODO: Add Doc Comment Here! pub fn create(&self) -> ZingoConfig { let lightwalletd_uri = self.lightwalletd_uri.clone().unwrap_or_default(); @@ -225,6 +242,8 @@ impl ZingoConfigBuilder { wallet_dir: self.wallet_dir.clone(), wallet_name: DEFAULT_WALLET_NAME.into(), logfile_name: DEFAULT_LOGFILE_NAME.into(), + #[cfg(feature = "ledger-support")] + use_ledger: false, } } } @@ -239,6 +258,8 @@ impl Default for ZingoConfigBuilder { wallet_name: None, logfile_name: None, chain: ChainType::Mainnet, + #[cfg(feature = "ledger-support")] + use_ledger: false, } } } diff --git a/zingolib/src/testutils/scenarios.rs b/zingolib/src/testutils/scenarios.rs index 16282bf52e..272897979e 100644 --- a/zingolib/src/testutils/scenarios.rs +++ b/zingolib/src/testutils/scenarios.rs @@ -208,6 +208,8 @@ pub mod setup { Some(conf_path), crate::config::ChainType::Regtest(regtest_network), true, + #[cfg(feature = "ledger-support")] + false, ) .unwrap() } diff --git a/zingolib/src/wallet.rs b/zingolib/src/wallet.rs index 381fd75349..ef0e851a04 100644 --- a/zingolib/src/wallet.rs +++ b/zingolib/src/wallet.rs @@ -175,6 +175,9 @@ pub enum WalletBase { Ufvk(String), /// Unified spending key Usk(Vec), + #[cfg(feature = "ledger-support")] + /// A hardware wallet + Ledger } impl WalletBase { @@ -381,6 +384,18 @@ impl LightWallet { )?; (wc, None) } + #[cfg(feature = "ledger-support")] + WalletBase::Ledger => { + let wc = WalletCapability::new_with_ledger(&config).map_err( + |e| { + Error::new( + ErrorKind::Other, + format!("Error initilizing Ledger Device: {}", e), + ) + }, + )?; + (wc, None) + } }; if let Err(e) = wc.new_address(wc.can_view(), false) { diff --git a/zingolib/src/wallet/disk/testing.rs b/zingolib/src/wallet/disk/testing.rs index 282da56425..e86c8b52ec 100644 --- a/zingolib/src/wallet/disk/testing.rs +++ b/zingolib/src/wallet/disk/testing.rs @@ -20,6 +20,8 @@ impl LightWallet { None, crate::config::ChainType::Regtest(crate::config::RegtestNetwork::all_upgrades_active()), true, + #[cfg(feature = "ledger-support")] + false, ) .unwrap(); Self::read_internal(data, &config) diff --git a/zingolib/src/wallet/disk/testing/tests.rs b/zingolib/src/wallet/disk/testing/tests.rs index ed73b3f7d9..cc2ce54e33 100644 --- a/zingolib/src/wallet/disk/testing/tests.rs +++ b/zingolib/src/wallet/disk/testing/tests.rs @@ -4,8 +4,7 @@ use zcash_client_backend::{PoolType, ShieldedProtocol}; use zcash_keys::keys::Era; use crate::{ - lightclient::LightClient, - wallet::{ + lightclient::LightClient, wallet::{ disk::testing::{ assert_wallet_capability_matches_seed, examples::{ @@ -13,10 +12,8 @@ use crate::{ HospitalMuseumVersion, HotelHumorVersion, MainnetSeedVersion, MobileShuffleVersion, NetworkSeedVersion, RegtestSeedVersion, TestnetSeedVersion, VillageTargetVersion, }, - }, - keys::unified::UnifiedKeyStore, - LightWallet, - }, + }, error::KeyError, keys::unified::UnifiedKeyStore, LightWallet + } }; // moving toward completeness: each of these tests should assert everything known about the LightWallet without network. @@ -299,3 +296,63 @@ async fn reload_wallet_from_buffer() { let balance = client.do_balance().await; assert_eq!(balance.orchard_balance, Some(10342837)); } + +#[cfg(feature = "ledger-support")] +#[tokio::test] +async fn test_ledger_initialization() { + use crate::wallet::WalletCapability; + + let mid_wallet = + NetworkSeedVersion::Testnet(TestnetSeedVersion::ChimneyBetter(ChimneyBetterVersion::V28)) + .load_example_wallet_with_verification() + .await; + + let mid_client = LightClient::create_from_wallet_async(mid_wallet) + .await + .unwrap(); + + let mut config = mid_client.wallet.transaction_context.config; + config.use_ledger = true; + + let expected_wc = WalletCapability::new_with_ledger(&config); + assert!(expected_wc.is_ok()); + + match expected_wc { + Ok(w) => { + assert!(w.is_ledger()) + }, + Err(_) => assert!(false), + } + +} + +#[cfg(feature = "ledger-support")] +#[tokio::test] +async fn test_ledger_initialization_fails_with_wrong_config() { + use crate::wallet::WalletCapability; + + let mid_wallet = + NetworkSeedVersion::Testnet(TestnetSeedVersion::ChimneyBetter(ChimneyBetterVersion::V28)) + .load_example_wallet_with_verification() + .await; + + let mid_client = LightClient::create_from_wallet_async(mid_wallet) + .await + .unwrap(); + + let mut config = mid_client.wallet.transaction_context.config; + config.use_ledger = false; // this should make things fail + + let expected_wc = WalletCapability::new_with_ledger(&config); + assert!(expected_wc.is_err()); + + match expected_wc { + Ok(_) => { + assert!(false) + }, + Err(e) => match e { + KeyError::LedgerNotSet => assert!(true), + _ => assert!(false) + } + } +} \ No newline at end of file diff --git a/zingolib/src/wallet/error.rs b/zingolib/src/wallet/error.rs index 151d16017e..c53c98076b 100644 --- a/zingolib/src/wallet/error.rs +++ b/zingolib/src/wallet/error.rs @@ -77,4 +77,8 @@ pub enum KeyError { /// Invalid format #[error("Viewing keys must be imported in the unified format")] InvalidFormat, + #[cfg(feature = "ledger-support")] + #[error("Ledger device not set.")] + /// Ledger flag was not set or device not found + LedgerNotSet } diff --git a/zingolib/src/wallet/keys.rs b/zingolib/src/wallet/keys.rs index 66667ec351..bbce0a3d19 100644 --- a/zingolib/src/wallet/keys.rs +++ b/zingolib/src/wallet/keys.rs @@ -15,7 +15,10 @@ use zcash_primitives::{ pub mod legacy; pub mod unified; +#[cfg(feature = "ledger-support")] +pub mod ledger; +mod capability; /// Sha256(Sha256(value)) pub fn double_sha256(payload: &[u8]) -> Vec { let h1 = ::digest(payload); diff --git a/zingolib/src/wallet/keys/capability.rs b/zingolib/src/wallet/keys/capability.rs new file mode 100644 index 0000000000..81fdac4bf6 --- /dev/null +++ b/zingolib/src/wallet/keys/capability.rs @@ -0,0 +1,333 @@ +//! This implements the Internal Capability + +use std::{collections::{HashMap, HashSet}, sync::{atomic, Arc}}; + +use append_only_vec::AppendOnlyVec; +use zcash_keys::{address::UnifiedAddress, keys::DerivationError}; +use zcash_primitives::{consensus::NetworkConstants, legacy::{keys::{AccountPubKey, IncomingViewingKey, NonHardenedChildIndex}, TransparentAddress}}; +use zip32::DiversifierIndex; +use crate::{config::ChainType, wallet::error::KeyError}; +use super::{legacy::generate_transparent_address_from_legacy_key, unified::{ReceiverSelection, UnifiedKeyStore, WalletCapability}, ToBase58Check}; + +pub (crate) trait InternalCapability: std::fmt::Debug + Send + Sync { + fn get_ua_from_contained_transparent_receiver( + &self, + capability: &WalletCapability, + receiver: &TransparentAddress, + ) -> Option; + + fn addresses<'a>(&'a self, capability: &'a WalletCapability) -> &'a AppendOnlyVec; + + fn transparent_child_addresses<'a>( + &'a self, + capability: &'a WalletCapability + ) -> &'a Arc>; + + fn new_address( + &self, + capability: &WalletCapability, + desired_receivers: ReceiverSelection, + legacy_key: bool, + ) -> Result; + + fn generate_transparent_receiver( + &self, + capability: &WalletCapability, + // this should only be `true` when generating transparent addresses while loading from legacy keys (pre wallet version 29) + // legacy transparent keys are already derived to the external scope so setting `legacy_key` to `true` will skip this scope derivation + legacy_key: bool, + ) -> Result, bip32::Error>; + + /// TODO: Add Doc Comment Here! + #[deprecated(note = "not used in zingolib codebase")] + fn get_taddr_to_secretkey_map( + &self, + capability: &WalletCapability, + chain: &ChainType, + ) -> Result, KeyError>; + + fn get_external_taddrs( + &self, + capability: &WalletCapability, + chain: &crate::config::ChainType + ) -> HashSet; + + fn get_taddrs(&self, + capability: &WalletCapability, + chain: &crate::config::ChainType + ) -> HashSet; + + fn first_sapling_address(&self, capability: &WalletCapability) -> sapling_crypto::PaymentAddress; + + fn can_view(&self, capability: &WalletCapability) -> ReceiverSelection; +} + +#[derive(Debug)] +pub (crate) struct InMemoryWallet {} + +impl InMemoryWallet { + pub(crate) fn new() -> InMemoryWallet { + InMemoryWallet{} + } +} +impl InternalCapability for InMemoryWallet { + fn get_ua_from_contained_transparent_receiver( + &self, + capability: &WalletCapability, + receiver: &TransparentAddress, + ) -> Option { + capability.unified_addresses + .iter() + .find(|ua| ua.transparent() == Some(receiver)) + .cloned() + } + /// TODO: Add Doc Comment Here! + fn addresses<'a>(&'a self, capability: &'a WalletCapability) -> &'a AppendOnlyVec { + &capability.unified_addresses + } + + /// TODO: Add Doc Comment Here! + fn transparent_child_addresses<'a>( + &'a self, + capability: &'a WalletCapability + ) -> &'a Arc> { + &capability.transparent_child_addresses + } + /// Generates a unified address from the given desired receivers + /// + /// See [`crate::wallet::WalletCapability::generate_transparent_receiver`] for information on using `legacy_key` + fn new_address( + &self, + capability: &WalletCapability, + desired_receivers: ReceiverSelection, + legacy_key: bool, + ) -> Result { + if capability + .addresses_write_lock + .swap(true, atomic::Ordering::Acquire) + { + return Err("addresses_write_lock collision!".to_string()); + } + + let previous_num_addresses = capability.unified_addresses.len(); + let orchard_receiver = if desired_receivers.orchard { + let fvk: orchard::keys::FullViewingKey = match capability.unified_key_store().try_into() { + Ok(viewkey) => viewkey, + Err(e) => { + capability.addresses_write_lock + .swap(false, atomic::Ordering::Release); + return Err(e.to_string()); + } + }; + Some(fvk.address_at(capability.unified_addresses.len(), orchard::keys::Scope::External)) + } else { + None + }; + + // produce a Sapling address to increment Sapling diversifier index + let sapling_receiver = if desired_receivers.sapling { + let mut sapling_diversifier_index = DiversifierIndex::new(); + let mut address; + let mut count = 0; + let fvk: sapling_crypto::zip32::DiversifiableFullViewingKey = + match capability.unified_key_store().try_into() { + Ok(viewkey) => viewkey, + Err(e) => { + capability.addresses_write_lock + .swap(false, atomic::Ordering::Release); + return Err(e.to_string()); + } + }; + loop { + (sapling_diversifier_index, address) = fvk + .find_address(sapling_diversifier_index) + .expect("Diversifier index overflow"); + sapling_diversifier_index + .increment() + .expect("Diversifier index overflow"); + // Not all sapling_diversifier_indexes produce valid + // sapling addresses. + // Because of this self.unified_addresses.len() + // will be <= sapling_diversifier_index + if count == capability.unified_addresses.len() { + break; + } + count += 1; + } + Some(address) + } else { + None + }; + + let transparent_receiver = if desired_receivers.transparent { + capability.generate_transparent_receiver(legacy_key) + .map_err(|e| e.to_string())? + } else { + None + }; + + let ua = UnifiedAddress::from_receivers( + orchard_receiver, + sapling_receiver, + transparent_receiver, + ); + let ua = match ua { + Some(address) => address, + None => { + capability.addresses_write_lock + .swap(false, atomic::Ordering::Release); + return Err( + "Invalid receivers requested! At least one of sapling or orchard required" + .to_string(), + ); + } + }; + capability.unified_addresses.push(ua.clone()); + assert_eq!(capability.unified_addresses.len(), previous_num_addresses + 1); + capability.addresses_write_lock + .swap(false, atomic::Ordering::Release); + Ok(ua) + } + + /// Generates a transparent receiver for the specified scope. + fn generate_transparent_receiver( + &self, + capability: &WalletCapability, + // this should only be `true` when generating transparent addresses while loading from legacy keys (pre wallet version 29) + // legacy transparent keys are already derived to the external scope so setting `legacy_key` to `true` will skip this scope derivation + legacy_key: bool, + ) -> Result, bip32::Error> { + let derive_address = |transparent_fvk: &AccountPubKey, + child_index: NonHardenedChildIndex| + -> Result { + let t_addr = if legacy_key { + generate_transparent_address_from_legacy_key(transparent_fvk, child_index)? + } else { + transparent_fvk + .derive_external_ivk()? + .derive_address(child_index)? + }; + + capability.transparent_child_addresses + .push((capability.addresses().len(), t_addr)); + Ok(t_addr) + }; + let child_index = NonHardenedChildIndex::from_index(capability.addresses().len() as u32) + .expect("hardened bit should not be set for non-hardened child indexes"); + let transparent_receiver = match capability.unified_key_store() { + UnifiedKeyStore::Spend(usk) => { + derive_address(&usk.transparent().to_account_pubkey(), child_index) + .map(Option::Some) + } + UnifiedKeyStore::View(ufvk) => ufvk + .transparent() + .map(|pub_key| derive_address(pub_key, child_index)) + .transpose(), + UnifiedKeyStore::Empty => Ok(None), + }?; + + Ok(transparent_receiver) + } + + /// TODO: Add Doc Comment Here! + fn get_taddr_to_secretkey_map( + &self, + capability: &WalletCapability, + chain: &ChainType, + ) -> Result, KeyError> { + if let UnifiedKeyStore::Spend(usk) = capability.unified_key_store() { + capability.transparent_child_addresses() + .iter() + .map(|(i, taddr)| -> Result<_, KeyError> { + let hash = match taddr { + TransparentAddress::PublicKeyHash(hash) => hash, + TransparentAddress::ScriptHash(hash) => hash, + }; + Ok(( + hash.to_base58check(&chain.b58_script_address_prefix(), &[]), + usk.transparent() + .derive_external_secret_key( + NonHardenedChildIndex::from_index(*i as u32) + .ok_or(KeyError::InvalidNonHardenedChildIndex)?, + ) + .map_err(DerivationError::Transparent) + .map_err(KeyError::KeyDerivationError)?, + )) + }) + .collect::>() + } else { + Err(KeyError::NoSpendCapability) + } + } + + /// external here refers to HD keys: + /// + /// where external and internal were inherited from the BIP44 conventions + fn get_external_taddrs( + &self, + capability: &WalletCapability, + chain: &crate::config::ChainType) -> HashSet { + capability.unified_addresses + .iter() + .filter_map(|address| { + address.transparent().and_then(|transparent_receiver| { + if let zcash_primitives::legacy::TransparentAddress::PublicKeyHash(hash) = + transparent_receiver + { + Some(super::ToBase58Check::to_base58check( + hash.as_slice(), + &chain.b58_pubkey_address_prefix(), + &[], + )) + } else { + None + } + }) + }) + .collect() + } + + /// TODO: This does not appear to be used + fn get_taddrs(&self, + capability: &WalletCapability, + chain: &crate::config::ChainType + ) -> HashSet { + self.get_external_taddrs(capability, chain,) + .union(&capability.get_rejection_address_set(chain)) + .cloned() + .collect() + } + /// TODO: Add Doc Comment Here! + fn first_sapling_address( + &self, + capability: &WalletCapability + ) -> sapling_crypto::PaymentAddress { + // This index is dangerous, but all ways to instantiate a UnifiedSpendAuthority + // create it with a suitable first address + *capability.addresses()[0].sapling().unwrap() + } + + /// Returns a selection of pools where the wallet can view funds. + fn can_view( + &self, + capability: &WalletCapability, + ) -> ReceiverSelection { + match &capability.unified_key_store { + UnifiedKeyStore::Spend(_) => ReceiverSelection { + orchard: true, + sapling: true, + transparent: true, + }, + UnifiedKeyStore::View(ufvk) => ReceiverSelection { + orchard: ufvk.orchard().is_some(), + sapling: ufvk.sapling().is_some(), + transparent: ufvk.transparent().is_some(), + }, + UnifiedKeyStore::Empty => ReceiverSelection { + orchard: false, + sapling: false, + transparent: false, + }, + } + } +} \ No newline at end of file diff --git a/zingolib/src/wallet/keys/ledger.rs b/zingolib/src/wallet/keys/ledger.rs new file mode 100644 index 0000000000..54a313bc63 --- /dev/null +++ b/zingolib/src/wallet/keys/ledger.rs @@ -0,0 +1,167 @@ +//! Holds information related to the ledger + + + +use std::io; +use secp256k1::{PublicKey, Secp256k1, SecretKey}; +use crate::wallet::{ + traits::ReadableWriteable, + keys::capability::InternalCapability, +}; + +/// Holds ledger things +#[derive(Debug)] +pub struct LedgerKeys { + ledger_id: PublicKey, + _app: ZcashApp +} + +//TODO! this is all mocked code +impl LedgerKeys { + /// TODO! this is all mocked code + pub fn new() -> LedgerKeys{ + // Create a new secp256k1 context + let secp = Secp256k1::new(); + + // Generate a secret key for testing purposes (not secure) + let secret_key = SecretKey::from_slice(&[0x01; 32]).expect("32 bytes, within curve order"); + + // Derive the corresponding public key + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + LedgerKeys { ledger_id: public_key, _app: ZcashApp::new() } + } +} + +/// Placeholder for the real thing +#[derive(Debug)] +pub struct ZcashApp {} + +impl ZcashApp { + fn new() -> ZcashApp { ZcashApp { }} +} + +impl ReadableWriteable for LedgerKeys { + const VERSION: u8 = 0; //not applicable + + fn read(mut reader: R, _input: ()) -> std::io::Result { + // let version = Self::read_version(&mut reader)?; + + // if version > Self::VERSION { + // let e = format!( + // "Don't know how to read ledger wallet version {}. Do you have the latest version?", + // version + // ); + // return Err(io::Error::new(io::ErrorKind::InvalidData, e)); + // } + + //retrieve the ledger id and verify it matches with the aocnnected device + let ledger_id = { + let mut buf = [0; secp256k1::constants::PUBLIC_KEY_SIZE]; + reader.read_exact(&mut buf)?; + + PublicKey::from_slice(&buf).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Bad public key stored for ledger id: {:?}", e), + ) + })? + }; + + // let app = Self::connect_ledger()?; + // // lets used futures simpler executor + // if ledger_id != futures::executor::block_on(Self::get_id(&app))? { + // return Err(io::Error::new( + // io::ErrorKind::InvalidInput, + // "Detected different ledger than used previously".to_string(), + // )); + // } + Ok(LedgerKeys { ledger_id, _app: ZcashApp::new()}) + + } + + fn write(&self, mut writer: W, _input: ()) -> std::io::Result<()> { + let id = self.ledger_id.serialize(); + writer.write_all(&id)?; + + Ok(()) + } +} + +#[derive(Debug)] +pub (crate) struct LedgerCapability {} + +impl LedgerCapability { + pub(crate) fn new() -> LedgerCapability { + LedgerCapability {} + } +} +impl InternalCapability for LedgerCapability { + fn get_ua_from_contained_transparent_receiver( + &self, + capability: &super::unified::WalletCapability, + receiver: &zcash_primitives::legacy::TransparentAddress, + ) -> Option { + todo!() + } + + fn addresses(&self, capability: &super::unified::WalletCapability) -> &append_only_vec::AppendOnlyVec { + todo!() + } + + fn transparent_child_addresses( + &self, + capability: &super::unified::WalletCapability + ) -> &std::sync::Arc> { + todo!() + } + + fn new_address( + &self, + capability: &super::unified::WalletCapability, + desired_receivers: super::unified::ReceiverSelection, + legacy_key: bool, + ) -> Result { + todo!() + } + + fn generate_transparent_receiver( + &self, + capability: &super::unified::WalletCapability, + // this should only be `true` when generating transparent addresses while loading from legacy keys (pre wallet version 29) + // legacy transparent keys are already derived to the external scope so setting `legacy_key` to `true` will skip this scope derivation + legacy_key: bool, + ) -> Result, bip32::Error> { + todo!() + } + + fn get_taddr_to_secretkey_map( + &self, + capability: &super::unified::WalletCapability, + chain: &crate::config::ChainType, + ) -> Result, crate::wallet::error::KeyError> { + todo!() + } + + fn first_sapling_address(&self, capability: &super::unified::WalletCapability) -> sapling_crypto::PaymentAddress { + todo!() + } + + fn can_view(&self, capability: &super::unified::WalletCapability) -> super::unified::ReceiverSelection { + todo!() + } + + fn get_external_taddrs( + &self, + capability: &super::unified::WalletCapability, + chain: &crate::config::ChainType + ) -> std::collections::HashSet { + todo!() + } + + fn get_taddrs(&self, + capability: &super::unified::WalletCapability, + chain: &crate::config::ChainType + ) -> std::collections::HashSet { + todo!() + } +} \ No newline at end of file diff --git a/zingolib/src/wallet/keys/unified.rs b/zingolib/src/wallet/keys/unified.rs index 8ac01d427a..7fb3e5db36 100644 --- a/zingolib/src/wallet/keys/unified.rs +++ b/zingolib/src/wallet/keys/unified.rs @@ -1,6 +1,5 @@ //! TODO: Add Mod Description Here! -use std::sync::atomic; use std::{ collections::{HashMap, HashSet}, io::{self, Read, Write}, @@ -19,13 +18,10 @@ use zcash_client_backend::address::UnifiedAddress; use zcash_client_backend::keys::{Era, UnifiedSpendingKey}; use zcash_client_backend::wallet::TransparentAddressMetadata; use zcash_encoding::{CompactSize, Vector}; -use zcash_keys::keys::{DerivationError, UnifiedFullViewingKey}; +use zcash_keys::keys::UnifiedFullViewingKey; use zcash_primitives::consensus::{NetworkConstants, Parameters}; -use zcash_primitives::legacy::{ - keys::{AccountPubKey, IncomingViewingKey, NonHardenedChildIndex}, - TransparentAddress, -}; -use zcash_primitives::zip32::{AccountId, DiversifierIndex}; +use zcash_primitives::legacy::TransparentAddress; +use zcash_primitives::zip32::AccountId; use crate::wallet::error::KeyError; use crate::wallet::traits::{DomainWalletExt, ReadableWriteable, Recipient}; @@ -34,15 +30,18 @@ use crate::{ wallet::data::new_rejection_address, }; -use super::legacy::{generate_transparent_address_from_legacy_key, legacy_sks_to_usk, Capability}; -use super::ToBase58Check; +#[cfg(feature = "ledger-support")] +use super::ledger::LedgerKeys; + +use super::legacy::{legacy_sks_to_usk, Capability}; +use crate::wallet::keys::capability::{InternalCapability, InMemoryWallet}; pub(crate) const KEY_TYPE_EMPTY: u8 = 0; pub(crate) const KEY_TYPE_VIEW: u8 = 1; pub(crate) const KEY_TYPE_SPEND: u8 = 2; /// In-memory store for wallet spending or viewing keys -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum UnifiedKeyStore { /// Wallet with spend capability Spend(Box), @@ -218,28 +217,35 @@ impl TryFrom<&UnifiedKeyStore> for zcash_primitives::legacy::keys::AccountPubKey /// or a [`zcash_keys::keys::UnifiedFullViewingKey`].

/// In addition to fundamental spending and viewing keys, the type caches generated addresses. pub struct WalletCapability { + + #[cfg(feature = "ledger-support")] + pub(crate) ledger: Option, /// Unified key store - pub unified_key_store: UnifiedKeyStore, + pub (crate) unified_key_store: UnifiedKeyStore, /// Cache of transparent addresses that the user has created. /// Receipts to a single address are correlated on chain. /// TODO: Is there any reason to have this field, apart from the /// unified_addresses field? - transparent_child_addresses: Arc>, + pub(crate) transparent_child_addresses: Arc>, // TODO: read/write for ephmereral addresses // TODO: Remove this field and exclusively use the TxMap field instead - rejection_addresses: Arc>, + pub(crate) rejection_addresses: Arc>, /// Cache of unified_addresses - unified_addresses: append_only_vec::AppendOnlyVec, - addresses_write_lock: AtomicBool, + pub(crate) unified_addresses: append_only_vec::AppendOnlyVec, + pub(crate) addresses_write_lock: AtomicBool, + pub(crate) capability: Box, } impl Default for WalletCapability { fn default() -> Self { Self { + #[cfg(feature = "ledger-support")] + ledger: None, unified_key_store: UnifiedKeyStore::Empty, transparent_child_addresses: Arc::new(AppendOnlyVec::new()), rejection_addresses: Arc::new(AppendOnlyVec::new()), unified_addresses: AppendOnlyVec::new(), addresses_write_lock: AtomicBool::new(false), + capability: Box::new(InMemoryWallet::new()), } } } @@ -306,23 +312,30 @@ fn read_write_receiver_selections() { } impl WalletCapability { + /// returns reference of Unified Key Store + pub fn unified_key_store(&self) -> &UnifiedKeyStore { + &self.unified_key_store + } + #[cfg(feature = "ledger-support")] + /// checks whether this WalletCapability is a Ledger device or not + pub fn is_ledger(&self) -> bool { + self.ledger.is_some() + } + pub(crate) fn get_ua_from_contained_transparent_receiver( &self, receiver: &TransparentAddress, ) -> Option { - self.unified_addresses - .iter() - .find(|ua| ua.transparent() == Some(receiver)) - .cloned() + self.capability.get_ua_from_contained_transparent_receiver(self, receiver) } /// TODO: Add Doc Comment Here! pub fn addresses(&self) -> &AppendOnlyVec { - &self.unified_addresses + self.capability.addresses(&self) } /// TODO: Add Doc Comment Here! pub fn transparent_child_addresses(&self) -> &Arc> { - &self.transparent_child_addresses + self.capability.transparent_child_addresses(&self) } /// Generates a unified address from the given desired receivers /// @@ -332,91 +345,7 @@ impl WalletCapability { desired_receivers: ReceiverSelection, legacy_key: bool, ) -> Result { - if self - .addresses_write_lock - .swap(true, atomic::Ordering::Acquire) - { - return Err("addresses_write_lock collision!".to_string()); - } - - let previous_num_addresses = self.unified_addresses.len(); - let orchard_receiver = if desired_receivers.orchard { - let fvk: orchard::keys::FullViewingKey = match (&self.unified_key_store).try_into() { - Ok(viewkey) => viewkey, - Err(e) => { - self.addresses_write_lock - .swap(false, atomic::Ordering::Release); - return Err(e.to_string()); - } - }; - Some(fvk.address_at(self.unified_addresses.len(), orchard::keys::Scope::External)) - } else { - None - }; - - // produce a Sapling address to increment Sapling diversifier index - let sapling_receiver = if desired_receivers.sapling { - let mut sapling_diversifier_index = DiversifierIndex::new(); - let mut address; - let mut count = 0; - let fvk: sapling_crypto::zip32::DiversifiableFullViewingKey = - match (&self.unified_key_store).try_into() { - Ok(viewkey) => viewkey, - Err(e) => { - self.addresses_write_lock - .swap(false, atomic::Ordering::Release); - return Err(e.to_string()); - } - }; - loop { - (sapling_diversifier_index, address) = fvk - .find_address(sapling_diversifier_index) - .expect("Diversifier index overflow"); - sapling_diversifier_index - .increment() - .expect("Diversifier index overflow"); - // Not all sapling_diversifier_indexes produce valid - // sapling addresses. - // Because of this self.unified_addresses.len() - // will be <= sapling_diversifier_index - if count == self.unified_addresses.len() { - break; - } - count += 1; - } - Some(address) - } else { - None - }; - - let transparent_receiver = if desired_receivers.transparent { - self.generate_transparent_receiver(legacy_key) - .map_err(|e| e.to_string())? - } else { - None - }; - - let ua = UnifiedAddress::from_receivers( - orchard_receiver, - sapling_receiver, - transparent_receiver, - ); - let ua = match ua { - Some(address) => address, - None => { - self.addresses_write_lock - .swap(false, atomic::Ordering::Release); - return Err( - "Invalid receivers requested! At least one of sapling or orchard required" - .to_string(), - ); - } - }; - self.unified_addresses.push(ua.clone()); - assert_eq!(self.unified_addresses.len(), previous_num_addresses + 1); - self.addresses_write_lock - .swap(false, atomic::Ordering::Release); - Ok(ua) + self.capability.new_address(&self, desired_receivers, legacy_key) } /// Generates a transparent receiver for the specified scope. @@ -426,36 +355,7 @@ impl WalletCapability { // legacy transparent keys are already derived to the external scope so setting `legacy_key` to `true` will skip this scope derivation legacy_key: bool, ) -> Result, bip32::Error> { - let derive_address = |transparent_fvk: &AccountPubKey, - child_index: NonHardenedChildIndex| - -> Result { - let t_addr = if legacy_key { - generate_transparent_address_from_legacy_key(transparent_fvk, child_index)? - } else { - transparent_fvk - .derive_external_ivk()? - .derive_address(child_index)? - }; - - self.transparent_child_addresses - .push((self.addresses().len(), t_addr)); - Ok(t_addr) - }; - let child_index = NonHardenedChildIndex::from_index(self.addresses().len() as u32) - .expect("hardened bit should not be set for non-hardened child indexes"); - let transparent_receiver = match &self.unified_key_store { - UnifiedKeyStore::Spend(usk) => { - derive_address(&usk.transparent().to_account_pubkey(), child_index) - .map(Option::Some) - } - UnifiedKeyStore::View(ufvk) => ufvk - .transparent() - .map(|pub_key| derive_address(pub_key, child_index)) - .transpose(), - UnifiedKeyStore::Empty => Ok(None), - }?; - - Ok(transparent_receiver) + self.capability.generate_transparent_receiver(&self, legacy_key) } /// TODO: Add Doc Comment Here! @@ -464,29 +364,7 @@ impl WalletCapability { &self, chain: &ChainType, ) -> Result, KeyError> { - if let UnifiedKeyStore::Spend(usk) = &self.unified_key_store { - self.transparent_child_addresses() - .iter() - .map(|(i, taddr)| -> Result<_, KeyError> { - let hash = match taddr { - TransparentAddress::PublicKeyHash(hash) => hash, - TransparentAddress::ScriptHash(hash) => hash, - }; - Ok(( - hash.to_base58check(&chain.b58_pubkey_address_prefix(), &[]), - usk.transparent() - .derive_external_secret_key( - NonHardenedChildIndex::from_index(*i as u32) - .ok_or(KeyError::InvalidNonHardenedChildIndex)?, - ) - .map_err(DerivationError::Transparent) - .map_err(KeyError::KeyDerivationError)?, - )) - }) - .collect::>() - } else { - Err(KeyError::NoSpendCapability) - } + self.capability.get_taddr_to_secretkey_map(&self, chain) } /// TODO: Add Doc Comment Here! @@ -551,41 +429,31 @@ impl WalletCapability { }) } - /// external here refers to HD keys: - /// - /// where external and internal were inherited from the BIP44 conventions - fn get_external_taddrs(&self, chain: &crate::config::ChainType) -> HashSet { - self.unified_addresses - .iter() - .filter_map(|address| { - address.transparent().and_then(|transparent_receiver| { - if let zcash_primitives::legacy::TransparentAddress::PublicKeyHash(hash) = - transparent_receiver - { - Some(super::ToBase58Check::to_base58check( - hash.as_slice(), - &chain.b58_pubkey_address_prefix(), - &[], - )) - } else { - None - } - }) - }) - .collect() + #[cfg(feature = "ledger-support")] + /// initializes a new wallet with a ledger + pub fn new_with_ledger(config: &ZingoConfig) -> Result { + use super::ledger::LedgerCapability; + + if !config.use_ledger { + return Err(KeyError::LedgerNotSet) + } + + let mut wc = WalletCapability::default(); + + wc.ledger = Some(LedgerKeys::new()); + wc.capability = Box::new(LedgerCapability::new()); + + Ok(wc) } + /// TODO: This does not appear to be used pub(crate) fn get_taddrs(&self, chain: &crate::config::ChainType) -> HashSet { - self.get_external_taddrs(chain) - .union(&self.get_rejection_address_set(chain)) - .cloned() - .collect() + self.capability.get_taddrs(&self, chain) } + /// TODO: Add Doc Comment Here! pub fn first_sapling_address(&self) -> sapling_crypto::PaymentAddress { - // This index is dangerous, but all ways to instantiate a UnifiedSpendAuthority - // create it with a suitable first address - *self.addresses()[0].sapling().unwrap() + self.capability.first_sapling_address(&self) } /// TODO: Add Doc Comment Here! @@ -621,7 +489,7 @@ impl WalletCapability { } impl ReadableWriteable for WalletCapability { - const VERSION: u8 = 4; + const VERSION: u8 = 5; fn read(mut reader: R, input: ChainType) -> io::Result { let version = Self::get_version(&mut reader)?; @@ -758,6 +626,15 @@ impl ReadableWriteable for WalletCapability { ..Default::default() } } + 5 => { + legacy_key = false; + length_of_rejection_addresses = reader.read_u32::()?; + + Self { + unified_key_store: UnifiedKeyStore::read(&mut reader, input)?, + ..Default::default() + } + } _ => { return Err(io::Error::new( io::ErrorKind::InvalidData,