Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ transparent = { package = "zcash_transparent", version = "0.6", features = ["tes
zcash_address = "0.10"
zcash_client_backend = { version = "0.21", features = ["lightwalletd-tonic-tls-webpki-roots", "orchard", "pczt", "tor"] }
zcash_client_sqlite = { version = "0.19", features = ["unstable", "orchard", "serde"] }
zcash_keys = { version = "0.12", features = ["unstable", "orchard"] }
zcash_keys = { version = "0.12", features = ["orchard", "unstable", "unstable-frost"] }
zcash_primitives = "0.26"
zcash_proofs = { version = "0.26", features = ["bundled-prover"] }
zcash_protocol = { version = "0.7", features = ["local-consensus"] }
Expand Down
3 changes: 3 additions & 0 deletions src/commands/pczt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use clap::Subcommand;
pub(crate) mod combine;
pub(crate) mod create;
pub(crate) mod create_manual;
pub(crate) mod create_max;
pub(crate) mod inspect;
pub(crate) mod pay_manual;
pub(crate) mod prove;
Expand All @@ -20,6 +21,8 @@ pub(crate) mod qr;
pub(crate) enum Command {
/// Create a PCZT
Create(create::Command),
/// Create a PCZT that sends all funds within the account
CreateMax(create_max::Command),
/// Create a shielding PCZT
Shield(shield::Command),
/// Create a PCZT from manually-provided transparent inputs
Expand Down
98 changes: 98 additions & 0 deletions src/commands/pczt/create_max.rs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume that this is required for the shielding operation? Does propose_send_max_transfer spend transparent UTXOs?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, actually, I guess it's for the spend that's required to return the shielded UTXO to the transparent multisig address?

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::str::FromStr;

use clap::Args;
use rand::rngs::OsRng;
use tokio::io::{stdout, AsyncWriteExt};
use uuid::Uuid;

use zcash_address::ZcashAddress;
use zcash_client_backend::{
data_api::{
wallet::{create_pczt_from_proposal, propose_send_max_transfer, ConfirmationsPolicy},
Account as _, MaxSpendMode,
},
fees::StandardFeeRule,
wallet::OvkPolicy,
};
use zcash_client_sqlite::{util::SystemClock, WalletDb};
use zcash_protocol::{
memo::{Memo, MemoBytes},
ShieldedProtocol,
};

use crate::{commands::select_account, config::WalletConfig, data::get_db_paths, error};

// Options accepted for the `pczt create-max` command
#[derive(Debug, Args)]
pub(crate) struct Command {
/// The UUID of the account to send funds from
account_id: Option<Uuid>,

/// The recipient's Unified, Sapling or transparent address
#[arg(long)]
address: String,

/// A memo to send to the recipient
#[arg(long)]
memo: Option<String>,

/// Spend all _currently_ spendable funds where it could be the case that the wallet
/// has received other funds that are not confirmed and therefore not spendable yet
/// and the caller evaluates that as an acceptable scenario.
///
/// Default is to spend **all funds**, failing if there are unspendable funds in the
/// wallet or if the wallet is not yet synced.
#[arg(long)]
only_spendable: bool,
}

impl Command {
pub(crate) async fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> {
let config = WalletConfig::read(wallet_dir.as_ref())?;
let params = config.network();

let (_, db_data) = get_db_paths(wallet_dir.as_ref());
let mut db_data = WalletDb::for_path(db_data, params, SystemClock, OsRng)?;
let account = select_account(&db_data, self.account_id)?;

let recipient =
ZcashAddress::from_str(&self.address).map_err(|_| error::Error::InvalidRecipient)?;
let memo = self
.memo
.map(|memo| Memo::from_str(&memo))
.transpose()?
.map(MemoBytes::from);
let mode = if self.only_spendable {
MaxSpendMode::MaxSpendable
} else {
MaxSpendMode::Everything
};

// Create the PCZT.
let proposal = propose_send_max_transfer(
&mut db_data,
&params,
account.id(),
&[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard],
&StandardFeeRule::Zip317,
recipient,
memo,
mode,
ConfirmationsPolicy::default(),
)
.map_err(error::Error::SendMax)?;

let pczt = create_pczt_from_proposal(
&mut db_data,
&params,
account.id(),
OvkPolicy::Sender,
&proposal,
)
.map_err(error::Error::from)?;

stdout().write_all(&pczt.serialize()).await?;

Ok(())
}
}
39 changes: 22 additions & 17 deletions src/commands/wallet/derive_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use clap::Args;
use secrecy::ExposeSecret;
use transparent::address::TransparentAddress;
use zcash_address::{ToAddress, ZcashAddress};
use zcash_keys::{address::UnifiedAddress, encoding::AddressCodec};
use zcash_keys::{
encoding::AddressCodec,
keys::{UnifiedAddressRequest, UnifiedFullViewingKey},
};
use zcash_protocol::{
consensus::{NetworkConstants, Parameters},
PoolType,
Expand Down Expand Up @@ -141,7 +144,7 @@ impl Command {
PoolType::SAPLING => {
// Print out Sapling information.
println!("Sapling derivation at {}:", self.path);
let address = match path.as_slice() {
let dfvk = match path.as_slice() {
[(32, true), subpath @ ..] => {
println!(" - ZIP 32 derivation path");
match subpath {
Expand Down Expand Up @@ -174,15 +177,15 @@ impl Command {
[] => {
// Only encode as an address if the network
// matches the wallet.
#[allow(deprecated)]
network_match.then(|| {
sapling::zip32::ExtendedSpendingKey::master(
seed.expose_secret(),
)
.derive_child(zip32::ChildIndex::hardened(32))
.derive_child(zip32::ChildIndex::hardened(*coin_type))
.derive_child(zip32::ChildIndex::hardened(*account))
.default_address()
.1
.to_extended_full_viewing_key()
})
}
_ => None,
Expand All @@ -195,7 +198,12 @@ impl Command {
None
}
};
if let Some(addr) = address {
if let Some(extfvk) = dfvk {
let addr = extfvk.default_address().1;
let ufvk =
UnifiedFullViewingKey::from_sapling_extended_full_viewing_key(extfvk)
.expect("always valid");
println!(" - UFVK: {}", ufvk.encode(&params));
println!(
" - Default address: {}",
ZcashAddress::from_sapling(params.network_type(), addr.to_bytes())
Expand All @@ -205,7 +213,7 @@ impl Command {
PoolType::ORCHARD => {
// Print out Orchard information.
println!("Orchard derivation at {}:", self.path);
let address = match path.as_slice() {
let fvk = match path.as_slice() {
[(32, true), subpath @ ..] => {
println!(" - ZIP 32 derivation path");
match subpath {
Expand Down Expand Up @@ -249,10 +257,7 @@ impl Command {
.ok()
})
.flatten()
.map(|sk| {
let fvk = orchard::keys::FullViewingKey::from(&sk);
fvk.address_at(0u32, zip32::Scope::External)
})
.map(|sk| orchard::keys::FullViewingKey::from(&sk))
}
_ => None,
}
Expand All @@ -264,13 +269,13 @@ impl Command {
None
}
};
if let Some(addr) = address {
println!(
" - Default address: {}",
UnifiedAddress::from_receivers(Some(addr), None, None)
.expect("valid")
.encode(&params),
);
if let Some(fvk) = fvk {
let ufvk = UnifiedFullViewingKey::from_orchard_fvk(fvk).expect("always valid");
let (ua, _) = ufvk
.default_address(UnifiedAddressRequest::AllAvailableKeys)
.expect("always valid");
println!(" - UFVK: {}", ufvk.encode(&params));
println!(" - Default address: {}", ua.encode(&params));
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use zcash_client_sqlite::{
};
use zcash_keys::keys::DerivationError;
use zcash_primitives::transaction::fees::zip317;
use zcash_protocol::value::BalanceError;
use zip321::Zip321Error;

pub(crate) type WalletErrorT = WalletError<
Expand All @@ -20,6 +21,15 @@ pub(crate) type WalletErrorT = WalletError<
ReceivedNoteId,
>;

pub(crate) type SendMaxErrorT = WalletError<
SqliteClientError,
commitment_tree::Error,
BalanceError,
zip317::FeeError,
zip317::FeeError,
ReceivedNoteId,
>;

pub(crate) type ShieldErrorT = WalletError<
SqliteClientError,
commitment_tree::Error,
Expand All @@ -39,6 +49,7 @@ pub enum Error {
InvalidKeysFile,
InvalidTreeState,
SendFailed { code: i32, reason: String },
SendMax(SendMaxErrorT),
Shield(ShieldErrorT),
TransparentMemo(usize),
Wallet(WalletErrorT),
Expand All @@ -56,6 +67,7 @@ impl fmt::Display for Error {
Error::InvalidKeysFile => write!(f, "Invalid keys file"),
Error::InvalidTreeState => write!(f, "Invalid TreeState received from server"),
Error::SendFailed { code, reason } => write!(f, "Send failed: ({code}) {reason}"),
Error::SendMax(e) => e.fmt(f),
Error::TransparentMemo(idx) => {
write!(f, "Payment {idx} invalid: can't send memo to a t-address")
}
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ fn main() -> Result<(), anyhow::Error> {
command,
}) => match command {
commands::pczt::Command::Create(command) => command.run(wallet_dir).await,
commands::pczt::Command::CreateMax(command) => command.run(wallet_dir).await,
commands::pczt::Command::Shield(command) => command.run(wallet_dir).await,
commands::pczt::Command::CreateManual(command) => command.run(wallet_dir).await,
commands::pczt::Command::PayManual(command) => command.run(wallet_dir).await,
Expand Down