Merge pull request #55 from Electric-Coin-Company/pczt
Add PCZT commands
This commit is contained in:
commit
40a92245cc
File diff suppressed because it is too large
Load Diff
33
Cargo.toml
33
Cargo.toml
|
@ -9,9 +9,11 @@ publish = false
|
|||
[dependencies]
|
||||
anyhow = "1"
|
||||
bip0039 = { version = "0.12", features = ["std", "all-languages"] }
|
||||
bip32 = "0.5"
|
||||
futures-util = "0.3"
|
||||
gumdrop = "0.8"
|
||||
hex = "0.4"
|
||||
jubjub = "0.10"
|
||||
prost = "0.13"
|
||||
rayon = "1.7"
|
||||
rusqlite = { version = "0.32", features = ["time"] }
|
||||
|
@ -27,10 +29,11 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
|||
uuid = "1"
|
||||
|
||||
orchard = { version = "0.10", default-features = false }
|
||||
pczt = "0.0"
|
||||
sapling = { package = "sapling-crypto", version = "0.3" }
|
||||
zcash_address = "0.6"
|
||||
zcash_client_backend = { version = "0.15", features = ["lightwalletd-tonic-tls-webpki-roots", "orchard", "tor"] }
|
||||
zcash_client_sqlite = { version = "0.13", features = ["unstable", "orchard"] }
|
||||
zcash_client_backend = { version = "0.15", features = ["lightwalletd-tonic-tls-webpki-roots", "orchard", "pczt", "tor"] }
|
||||
zcash_client_sqlite = { version = "0.13", features = ["unstable", "orchard", "serde"] }
|
||||
zcash_keys = { version = "0.5", features = ["unstable", "orchard"] }
|
||||
zcash_primitives = "0.20"
|
||||
zcash_proofs = "0.20"
|
||||
|
@ -45,6 +48,12 @@ age = { version = "0.11", features = ["armor", "plugin"] }
|
|||
iso_currency = { version = "0.5", features = ["with-serde"] }
|
||||
rust_decimal = "1"
|
||||
|
||||
# PCZT QR codes
|
||||
nokhwa = { version = "0.10", optional = true, features = ["input-native"] }
|
||||
qrcode = { version = "0.14", optional = true, default-features = false }
|
||||
rqrr = { version = "0.8", optional = true }
|
||||
ur = { version = "0.4", optional = true }
|
||||
|
||||
# TUI
|
||||
crossterm = { version = "0.28", optional = true, features = ["event-stream"] }
|
||||
ratatui = { version = "0.28", optional = true }
|
||||
|
@ -54,6 +63,7 @@ tui-logger = { version = "0.12", optional = true, features = ["tracing-support"]
|
|||
|
||||
[features]
|
||||
default = ["transparent-inputs"]
|
||||
pczt-qr = ["dep:nokhwa", "dep:qrcode", "dep:rqrr", "dep:ur"]
|
||||
transparent-inputs = [
|
||||
"zcash_client_sqlite/transparent-inputs",
|
||||
]
|
||||
|
@ -66,11 +76,14 @@ tui = [
|
|||
]
|
||||
|
||||
[patch.crates-io]
|
||||
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
||||
zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
||||
zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
||||
zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
||||
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
||||
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
||||
zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
||||
zip321 = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
||||
orchard = { git = "https://github.com/zcash/orchard.git", rev = "bcd08e1d23e70c42a338f3e3f79d6f4c0c219805" }
|
||||
pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||
sapling-crypto = { git = "https://github.com/zcash/sapling-crypto.git", rev = "29cff9683cdf2f0c522ff3224081dfb4fbc80248" }
|
||||
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||
zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||
zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||
zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||
zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||
zip321 = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||
|
|
|
@ -7,6 +7,7 @@ pub(crate) mod list_accounts;
|
|||
pub(crate) mod list_addresses;
|
||||
pub(crate) mod list_tx;
|
||||
pub(crate) mod list_unspent;
|
||||
pub(crate) mod pczt;
|
||||
pub(crate) mod propose;
|
||||
pub(crate) mod reset;
|
||||
pub(crate) mod send;
|
||||
|
|
|
@ -28,7 +28,7 @@ impl Command {
|
|||
println!("Account {}", self.account_id);
|
||||
let (ua, _) = account
|
||||
.uivk()
|
||||
.default_address(UnifiedAddressRequest::all().unwrap())?;
|
||||
.default_address(UnifiedAddressRequest::all())?;
|
||||
println!(" Default Address: {}", ua.encode(¶ms));
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
use gumdrop::Options;
|
||||
|
||||
pub(crate) mod combine;
|
||||
pub(crate) mod create;
|
||||
pub(crate) mod prove;
|
||||
pub(crate) mod send;
|
||||
pub(crate) mod sign;
|
||||
|
||||
#[cfg(feature = "pczt-qr")]
|
||||
pub(crate) mod qr;
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
pub(crate) enum Command {
|
||||
#[options(help = "create a PCZT")]
|
||||
Create(create::Command),
|
||||
#[options(help = "create proofs for a PCZT")]
|
||||
Prove(prove::Command),
|
||||
#[options(help = "apply signatures to a PCZT")]
|
||||
Sign(sign::Command),
|
||||
#[options(help = "combine two PCZTs")]
|
||||
Combine(combine::Command),
|
||||
#[options(help = "extract a finished transaction and send it")]
|
||||
Send(send::Command),
|
||||
#[cfg(feature = "pczt-qr")]
|
||||
#[options(help = "render a PCZT as an animated QR code")]
|
||||
ToQr(qr::Send),
|
||||
#[cfg(feature = "pczt-qr")]
|
||||
#[options(help = "read a PCZT from an animated QR code via the webcam")]
|
||||
FromQr(qr::Receive),
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use gumdrop::Options;
|
||||
use pczt::{roles::combiner::Combiner, Pczt};
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{stdout, AsyncReadExt, AsyncWriteExt},
|
||||
};
|
||||
|
||||
// Options accepted for the `pczt combine` command
|
||||
#[derive(Debug, Options)]
|
||||
pub(crate) struct Command {
|
||||
#[options(help = "a list of PCZT files to combine")]
|
||||
input: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub(crate) async fn run(self) -> Result<(), anyhow::Error> {
|
||||
let mut pczts = vec![];
|
||||
for f in self.input {
|
||||
let mut f = File::open(f).await?;
|
||||
|
||||
let mut buf = vec![];
|
||||
f.read_to_end(&mut buf).await?;
|
||||
|
||||
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
|
||||
|
||||
pczts.push(pczt);
|
||||
}
|
||||
|
||||
let pczt = Combiner::new(pczts)
|
||||
.combine()
|
||||
.map_err(|e| anyhow!("Failed to combine PCZTs: {:?}", e))?;
|
||||
|
||||
stdout().write_all(&pczt.serialize()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
#![allow(deprecated)]
|
||||
use std::{num::NonZeroUsize, str::FromStr};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use gumdrop::Options;
|
||||
|
||||
use tokio::io::{stdout, AsyncWriteExt};
|
||||
use zcash_address::ZcashAddress;
|
||||
use zcash_client_backend::{
|
||||
data_api::{
|
||||
wallet::{
|
||||
create_pczt_from_proposal, input_selection::GreedyInputSelector, propose_transfer,
|
||||
},
|
||||
WalletRead,
|
||||
},
|
||||
fees::{standard::MultiOutputChangeStrategy, DustOutputPolicy, SplitPolicy, StandardFeeRule},
|
||||
wallet::OvkPolicy,
|
||||
ShieldedProtocol,
|
||||
};
|
||||
use zcash_client_sqlite::WalletDb;
|
||||
use zcash_protocol::{
|
||||
memo::{Memo, MemoBytes},
|
||||
value::Zatoshis,
|
||||
};
|
||||
use zip321::{Payment, TransactionRequest};
|
||||
|
||||
use crate::{config::WalletConfig, data::get_db_paths, error, MIN_CONFIRMATIONS};
|
||||
|
||||
// Options accepted for the `pczt create` command
|
||||
#[derive(Debug, Options)]
|
||||
pub(crate) struct Command {
|
||||
#[options(
|
||||
required,
|
||||
help = "the recipient's Unified, Sapling or transparent address"
|
||||
)]
|
||||
address: String,
|
||||
|
||||
#[options(required, help = "the amount in zatoshis")]
|
||||
value: u64,
|
||||
|
||||
#[options(help = "a memo to send to the recipient")]
|
||||
memo: Option<String>,
|
||||
|
||||
#[options(
|
||||
help = "note management: the number of notes to maintain in the wallet",
|
||||
default = "4"
|
||||
)]
|
||||
target_note_count: usize,
|
||||
|
||||
#[options(
|
||||
help = "note management: the minimum allowed value for split change amounts",
|
||||
default = "10000000"
|
||||
)]
|
||||
min_split_output_value: u64,
|
||||
}
|
||||
|
||||
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)?;
|
||||
let account_id = *db_data
|
||||
.get_account_ids()?
|
||||
.first()
|
||||
.ok_or(anyhow!("Wallet has no accounts"))?;
|
||||
|
||||
// Create the PCZT.
|
||||
let change_strategy = MultiOutputChangeStrategy::new(
|
||||
StandardFeeRule::Zip317,
|
||||
None,
|
||||
ShieldedProtocol::Orchard,
|
||||
DustOutputPolicy::default(),
|
||||
SplitPolicy::with_min_output_value(
|
||||
NonZeroUsize::new(self.target_note_count)
|
||||
.ok_or(anyhow!("target note count must be nonzero"))?,
|
||||
Zatoshis::from_u64(self.min_split_output_value)?,
|
||||
),
|
||||
);
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
|
||||
let request = TransactionRequest::new(vec![Payment::new(
|
||||
ZcashAddress::from_str(&self.address).map_err(|_| error::Error::InvalidRecipient)?,
|
||||
Zatoshis::from_u64(self.value).map_err(|_| error::Error::InvalidAmount)?,
|
||||
self.memo
|
||||
.map(|memo| Memo::from_str(&memo))
|
||||
.transpose()?
|
||||
.map(MemoBytes::from),
|
||||
None,
|
||||
None,
|
||||
vec![],
|
||||
)
|
||||
.ok_or_else(|| anyhow!("Invalid memo"))?])
|
||||
.map_err(error::Error::from)?;
|
||||
|
||||
let proposal = propose_transfer(
|
||||
&mut db_data,
|
||||
¶ms,
|
||||
account_id,
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
request,
|
||||
MIN_CONFIRMATIONS,
|
||||
)
|
||||
.map_err(error::Error::from)?;
|
||||
|
||||
let pczt = create_pczt_from_proposal(
|
||||
&mut db_data,
|
||||
¶ms,
|
||||
account_id,
|
||||
OvkPolicy::Sender,
|
||||
&proposal,
|
||||
)
|
||||
.map_err(error::Error::from)?;
|
||||
|
||||
stdout().write_all(&pczt.serialize()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
use anyhow::anyhow;
|
||||
use gumdrop::Options;
|
||||
use pczt::{
|
||||
roles::{prover::Prover, updater::Updater},
|
||||
Pczt,
|
||||
};
|
||||
use sapling::ProofGenerationKey;
|
||||
use secrecy::ExposeSecret;
|
||||
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt};
|
||||
use zcash_keys::keys::UnifiedSpendingKey;
|
||||
use zcash_proofs::prover::LocalTxProver;
|
||||
use zcash_protocol::consensus::{NetworkConstants, Parameters};
|
||||
use zip32::fingerprint::SeedFingerprint;
|
||||
|
||||
use crate::config::WalletConfig;
|
||||
|
||||
// Options accepted for the `pczt prove` command
|
||||
#[derive(Debug, Options)]
|
||||
pub(crate) struct Command {
|
||||
#[options(
|
||||
help = "hex encoding of the Sapling proof generation key",
|
||||
parse(try_from_str = "hex::decode")
|
||||
)]
|
||||
sapling_proof_generation_key: Option<Vec<u8>>,
|
||||
|
||||
#[options(
|
||||
help = "age identity file to decrypt the mnemonic phrase with for deriving the Sapling proof generation key"
|
||||
)]
|
||||
identity: Option<String>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub(crate) async fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> {
|
||||
let mut buf = vec![];
|
||||
stdin().read_to_end(&mut buf).await?;
|
||||
|
||||
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
|
||||
|
||||
// If we have Sapling spends, we need Sapling proof generation keys.
|
||||
let pczt = if !pczt.sapling().spends().is_empty() {
|
||||
enum PgkSource {
|
||||
Provided(ProofGenerationKey),
|
||||
Wallet {
|
||||
config: WalletConfig,
|
||||
seed_fp: SeedFingerprint,
|
||||
},
|
||||
}
|
||||
|
||||
impl PgkSource {
|
||||
fn proof_generation_key(
|
||||
&self,
|
||||
derivation: Option<([u8; 32], Vec<zip32::ChildIndex>)>,
|
||||
) -> anyhow::Result<ProofGenerationKey> {
|
||||
match self {
|
||||
PgkSource::Provided(proof_generation_key) => {
|
||||
Ok(proof_generation_key.clone())
|
||||
}
|
||||
PgkSource::Wallet { config, seed_fp } => {
|
||||
if let Some((seed_fingerprint, derivation_path)) = derivation {
|
||||
let params = config.network();
|
||||
|
||||
if seed_fingerprint == seed_fp.to_bytes()
|
||||
&& derivation_path.len() == 3
|
||||
&& derivation_path[0] == zip32::ChildIndex::hardened(32)
|
||||
&& derivation_path[1]
|
||||
== zip32::ChildIndex::hardened(
|
||||
params.network_type().coin_type(),
|
||||
)
|
||||
{
|
||||
let account_index = zip32::AccountId::try_from(
|
||||
derivation_path[2].index() - (1 << 31),
|
||||
)
|
||||
.expect("valid");
|
||||
|
||||
let usk = UnifiedSpendingKey::from_seed(
|
||||
¶ms,
|
||||
config
|
||||
.seed()
|
||||
.ok_or(anyhow!(
|
||||
"Seed must be present to enable signing"
|
||||
))?
|
||||
.expose_secret(),
|
||||
account_index,
|
||||
)?;
|
||||
|
||||
Ok(usk.sapling().expsk.proof_generation_key())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Invalid ZIP 32 derivation path for PCZT Sapling spend"
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Missing ZIP 32 derivation path for PCZT Sapling spend"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pkg_source = match (self.sapling_proof_generation_key, self.identity) {
|
||||
(Some(proof_generation_key), _) => {
|
||||
if proof_generation_key.len() == 64 {
|
||||
Ok(PgkSource::Provided(sapling::keys::ProofGenerationKey {
|
||||
ak: sapling::keys::SpendValidatingKey::temporary_zcash_from_bytes(
|
||||
&proof_generation_key[..32],
|
||||
)
|
||||
.ok_or_else(|| anyhow!("Invalid Sapling proof generation key"))?,
|
||||
nsk: jubjub::Scalar::from_bytes(
|
||||
&proof_generation_key[32..].try_into().unwrap(),
|
||||
)
|
||||
.into_option()
|
||||
.ok_or_else(|| anyhow!("Invalid Sapling proof generation key"))?,
|
||||
}))
|
||||
} else {
|
||||
Err(anyhow!("Invalid Sapling proof generation key"))
|
||||
}
|
||||
}
|
||||
(None, Some(identity)) => {
|
||||
// Try to load it from the wallet config.
|
||||
let mut config = WalletConfig::read(wallet_dir.as_ref())?;
|
||||
|
||||
// Decrypt the mnemonic to access the seed.
|
||||
let identities = age::IdentityFile::from_file(identity)?.into_identities()?;
|
||||
config.decrypt(identities.iter().map(|i| i.as_ref() as _))?;
|
||||
|
||||
// Cache the seed fingerprint for matching.
|
||||
let seed = config
|
||||
.seed()
|
||||
.ok_or(anyhow!("Seed must be present to enable signing"))?
|
||||
.expose_secret();
|
||||
let seed_fingerprint = SeedFingerprint::from_seed(seed)
|
||||
.ok_or_else(|| anyhow!("Invalid seed length"))?;
|
||||
|
||||
Ok(PgkSource::Wallet {
|
||||
config,
|
||||
seed_fp: seed_fingerprint,
|
||||
})
|
||||
}
|
||||
(None, None) => Err(anyhow!(
|
||||
"Cannot create Sapling proofs without a proof generation key"
|
||||
)),
|
||||
}?;
|
||||
|
||||
// Add Sapling proof generation key.
|
||||
Updater::new(pczt)
|
||||
.update_sapling_with(|mut updater| {
|
||||
let non_dummy_spends = updater
|
||||
.bundle()
|
||||
.spends()
|
||||
.iter()
|
||||
.enumerate()
|
||||
// Dummy spends will already have a proof generation key.
|
||||
.filter(|(_, spend)| spend.proof_generation_key().is_none())
|
||||
.map(|(index, spend)| {
|
||||
(
|
||||
index,
|
||||
spend
|
||||
.zip32_derivation()
|
||||
.as_ref()
|
||||
.map(|d| (*d.seed_fingerprint(), d.derivation_path().clone())),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Assume all non-dummy spent notes are from the same account.
|
||||
for (index, derivation) in non_dummy_spends {
|
||||
updater.update_spend_with(index, |mut spend_updater| {
|
||||
spend_updater.set_proof_generation_key(
|
||||
pkg_source.proof_generation_key(derivation).unwrap(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| anyhow!("Failed to add Sapling proof generation key: {:?}", e))?
|
||||
.finish()
|
||||
} else {
|
||||
pczt
|
||||
};
|
||||
|
||||
let prover =
|
||||
LocalTxProver::with_default_location().ok_or(anyhow!("Missing Sapling parameters"))?;
|
||||
|
||||
let pczt = Prover::new(pczt)
|
||||
.create_orchard_proof(&orchard::circuit::ProvingKey::build())
|
||||
.map_err(|e| anyhow!("Failed to create Orchard proof: {:?}", e))?
|
||||
.create_sapling_proofs(&prover, &prover)
|
||||
.map_err(|e| anyhow!("Failed to create Sapling proofs: {:?}", e))?
|
||||
.finish();
|
||||
|
||||
stdout().write_all(&pczt.serialize()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use gumdrop::Options;
|
||||
use nokhwa::{
|
||||
pixel_format::LumaFormat,
|
||||
utils::{CameraIndex, RequestedFormat, RequestedFormatType},
|
||||
Camera,
|
||||
};
|
||||
use pczt::Pczt;
|
||||
use qrcode::{render::unicode, QrCode};
|
||||
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::ShutdownListener;
|
||||
|
||||
const ZCASH_PCZT: &str = "zcash-pczt";
|
||||
|
||||
// Options accepted for the `pczt to-qr` command
|
||||
#[derive(Debug, Options)]
|
||||
pub(crate) struct Send {
|
||||
#[options(
|
||||
help = "the duration in milliseconds to wait between QR codes (default is 500)",
|
||||
default = "500"
|
||||
)]
|
||||
interval: u64,
|
||||
}
|
||||
|
||||
impl Send {
|
||||
pub(crate) async fn run(self, mut shutdown: ShutdownListener) -> Result<(), anyhow::Error> {
|
||||
let mut buf = vec![];
|
||||
stdin().read_to_end(&mut buf).await?;
|
||||
|
||||
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
|
||||
|
||||
let mut encoder = ur::Encoder::new(&pczt.serialize(), 100, ZCASH_PCZT)
|
||||
.map_err(|e| anyhow!("Failed to build UR encoder: {e}"))?;
|
||||
|
||||
let mut stdout = stdout();
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(self.interval));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if shutdown.requested() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ur = encoder
|
||||
.next_part()
|
||||
.map_err(|e| anyhow!("Failed to encode PCZT part: {e}"))?;
|
||||
let code = QrCode::new(&ur.to_uppercase())?;
|
||||
let string = code
|
||||
.render::<unicode::Dense1x2>()
|
||||
.dark_color(unicode::Dense1x2::Dark)
|
||||
.light_color(unicode::Dense1x2::Light)
|
||||
.quiet_zone(false)
|
||||
.build();
|
||||
|
||||
stdout.write_all(format!("{string}\n").as_bytes()).await?;
|
||||
stdout.write_all(format!("{ur}\n\n\n\n").as_bytes()).await?;
|
||||
stdout.flush().await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Options accepted for the `pczt from-qr` command
|
||||
#[derive(Debug, Options)]
|
||||
pub(crate) struct Receive {
|
||||
#[options(
|
||||
help = "the duration in milliseconds to wait between scanning for QR codes (default is 500)",
|
||||
default = "500"
|
||||
)]
|
||||
interval: u64,
|
||||
}
|
||||
|
||||
impl Receive {
|
||||
pub(crate) async fn run(self, mut shutdown: ShutdownListener) -> Result<(), anyhow::Error> {
|
||||
let mut camera = Camera::new(
|
||||
CameraIndex::Index(0),
|
||||
RequestedFormat::new::<LumaFormat>(RequestedFormatType::AbsoluteHighestFrameRate),
|
||||
)?;
|
||||
let mut decoder = ur::Decoder::default();
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(self.interval));
|
||||
|
||||
while !decoder.complete() {
|
||||
interval.tick().await;
|
||||
|
||||
if shutdown.requested() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame = camera.frame()?;
|
||||
let decoded = frame.decode_image::<LumaFormat>()?;
|
||||
|
||||
let mut img = rqrr::PreparedImage::prepare(decoded);
|
||||
let grids = img.detect_grids();
|
||||
if let Some(grid) = grids.first() {
|
||||
let (_, content) = grid.decode()?;
|
||||
if content.starts_with("ur:zcash-pczt") {
|
||||
eprintln!("{content}");
|
||||
decoder
|
||||
.receive(&content)
|
||||
.map_err(|e| anyhow!("Failed to parse QR code: {:?}", e))?;
|
||||
} else {
|
||||
eprintln!("Unexpected UR type: {content}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pczt = Pczt::parse(
|
||||
&decoder
|
||||
.message()
|
||||
.map_err(|e| anyhow!("Failed to extract full message from QR codes: {:?}", e))?
|
||||
.expect("complete"),
|
||||
)
|
||||
.map_err(|e| anyhow!("Failed to read PCZT from QR codes: {:?}", e))?;
|
||||
|
||||
stdout().write_all(&pczt.serialize()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
use anyhow::anyhow;
|
||||
use gumdrop::Options;
|
||||
|
||||
use pczt::Pczt;
|
||||
use tokio::io::{stdin, AsyncReadExt};
|
||||
use zcash_client_backend::{
|
||||
data_api::{wallet::extract_and_store_transaction_from_pczt, WalletRead},
|
||||
proto::service,
|
||||
};
|
||||
use zcash_client_sqlite::WalletDb;
|
||||
use zcash_proofs::prover::LocalTxProver;
|
||||
|
||||
use crate::{
|
||||
config::WalletConfig,
|
||||
data::get_db_paths,
|
||||
error,
|
||||
remote::{tor_client, Servers},
|
||||
};
|
||||
|
||||
// Options accepted for the `pczt send` command
|
||||
#[derive(Debug, Options)]
|
||||
pub(crate) struct Command {
|
||||
#[options(
|
||||
help = "the server to send via (default is \"ecc\")",
|
||||
default = "ecc",
|
||||
parse(try_from_str = "Servers::parse")
|
||||
)]
|
||||
server: Servers,
|
||||
|
||||
#[options(help = "disable connections via TOR")]
|
||||
disable_tor: 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)?;
|
||||
|
||||
let server = self.server.pick(params)?;
|
||||
let mut client = if self.disable_tor {
|
||||
server.connect_direct().await?
|
||||
} else {
|
||||
server.connect(|| tor_client(wallet_dir.as_ref())).await?
|
||||
};
|
||||
|
||||
let mut buf = vec![];
|
||||
stdin().read_to_end(&mut buf).await?;
|
||||
|
||||
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
|
||||
|
||||
let prover =
|
||||
LocalTxProver::with_default_location().ok_or(anyhow!("Missing Sapling parameters"))?;
|
||||
let (spend_vk, output_vk) = prover.verifying_keys();
|
||||
|
||||
let txid = extract_and_store_transaction_from_pczt::<_, ()>(
|
||||
&mut db_data,
|
||||
pczt,
|
||||
&spend_vk,
|
||||
&output_vk,
|
||||
&orchard::circuit::VerifyingKey::build(),
|
||||
)
|
||||
.map_err(|e| anyhow!("Failed to extract and store transaction from PCZT: {:?}", e))?;
|
||||
|
||||
// Send the transaction.
|
||||
println!("Sending transaction...");
|
||||
let (txid, raw_tx) = db_data
|
||||
.get_transaction(txid)?
|
||||
.map(|tx| {
|
||||
let mut raw_tx = service::RawTransaction::default();
|
||||
tx.write(&mut raw_tx.data).unwrap();
|
||||
(tx.txid(), raw_tx)
|
||||
})
|
||||
.ok_or(anyhow!("Transaction not found for id {:?}", txid))?;
|
||||
let response = client.send_transaction(raw_tx).await?.into_inner();
|
||||
|
||||
if response.error_code != 0 {
|
||||
Err(error::Error::SendFailed {
|
||||
code: response.error_code,
|
||||
reason: response.error_message,
|
||||
}
|
||||
.into())
|
||||
} else {
|
||||
println!("{}", txid);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use gumdrop::Options;
|
||||
use pczt::{
|
||||
roles::{signer::Signer, updater::Updater},
|
||||
Pczt,
|
||||
};
|
||||
use secrecy::ExposeSecret;
|
||||
use tokio::io::{stdin, stdout, AsyncReadExt, AsyncWriteExt};
|
||||
use zcash_keys::keys::UnifiedSpendingKey;
|
||||
use zcash_primitives::legacy::keys::{NonHardenedChildIndex, TransparentKeyScope};
|
||||
use zcash_protocol::consensus::{NetworkConstants, Parameters};
|
||||
use zip32::fingerprint::SeedFingerprint;
|
||||
|
||||
use crate::config::WalletConfig;
|
||||
|
||||
// Options accepted for the `pczt sign` command
|
||||
#[derive(Debug, Options)]
|
||||
pub(crate) struct Command {
|
||||
#[options(help = "age identity file to decrypt the mnemonic phrase with")]
|
||||
identity: String,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub(crate) async fn run(self, wallet_dir: Option<String>) -> Result<(), anyhow::Error> {
|
||||
let mut config = WalletConfig::read(wallet_dir.as_ref())?;
|
||||
let params = config.network();
|
||||
|
||||
let mut buf = vec![];
|
||||
stdin().read_to_end(&mut buf).await?;
|
||||
|
||||
let pczt = Pczt::parse(&buf).map_err(|e| anyhow!("Failed to read PCZT: {:?}", e))?;
|
||||
|
||||
// Decrypt the mnemonic to access the seed.
|
||||
let identities = age::IdentityFile::from_file(self.identity)?.into_identities()?;
|
||||
config.decrypt(identities.iter().map(|i| i.as_ref() as _))?;
|
||||
|
||||
let seed = config
|
||||
.seed()
|
||||
.ok_or(anyhow!("Seed must be present to enable signing"))?
|
||||
.expose_secret();
|
||||
let seed_fp =
|
||||
SeedFingerprint::from_seed(seed).ok_or_else(|| anyhow!("Invalid seed length"))?;
|
||||
|
||||
// Find all the spends matching our seed. For now as a hack, we use the Updater
|
||||
// role to access the bundle data we need.
|
||||
enum KeyRef {
|
||||
Orchard {
|
||||
index: usize,
|
||||
},
|
||||
Sapling {
|
||||
index: usize,
|
||||
},
|
||||
Transparent {
|
||||
index: usize,
|
||||
scope: TransparentKeyScope,
|
||||
address_index: NonHardenedChildIndex,
|
||||
},
|
||||
}
|
||||
let mut keys = BTreeMap::<zip32::AccountId, Vec<KeyRef>>::new();
|
||||
let pczt = Updater::new(pczt)
|
||||
.update_orchard_with(|updater| {
|
||||
for (index, action) in updater.bundle().actions().iter().enumerate() {
|
||||
if let Some(derivation) = action.spend().zip32_derivation() {
|
||||
if derivation.seed_fingerprint() == &seed_fp.to_bytes()
|
||||
&& derivation.derivation_path().len() == 3
|
||||
&& derivation.derivation_path()[0] == zip32::ChildIndex::hardened(32)
|
||||
&& derivation.derivation_path()[1]
|
||||
== zip32::ChildIndex::hardened(params.network_type().coin_type())
|
||||
{
|
||||
let account_index = zip32::AccountId::try_from(
|
||||
derivation.derivation_path()[2].index() - (1 << 31),
|
||||
)
|
||||
.expect("valid");
|
||||
|
||||
keys.entry(account_index)
|
||||
.or_default()
|
||||
.push(KeyRef::Orchard { index });
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.expect("no errors")
|
||||
.update_sapling_with(|updater| {
|
||||
for (index, spend) in updater.bundle().spends().iter().enumerate() {
|
||||
if let Some(derivation) = spend.zip32_derivation() {
|
||||
if derivation.seed_fingerprint() == &seed_fp.to_bytes()
|
||||
&& derivation.derivation_path().len() == 3
|
||||
&& derivation.derivation_path()[0] == zip32::ChildIndex::hardened(32)
|
||||
&& derivation.derivation_path()[1]
|
||||
== zip32::ChildIndex::hardened(params.network_type().coin_type())
|
||||
{
|
||||
let account_index = zip32::AccountId::try_from(
|
||||
derivation.derivation_path()[2].index() - (1 << 31),
|
||||
)
|
||||
.expect("valid");
|
||||
|
||||
keys.entry(account_index)
|
||||
.or_default()
|
||||
.push(KeyRef::Sapling { index });
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.expect("no errors")
|
||||
.update_transparent_with(|updater| {
|
||||
for (index, input) in updater.bundle().inputs().iter().enumerate() {
|
||||
for derivation in input.bip32_derivation().values() {
|
||||
if derivation.seed_fingerprint() == &seed_fp.to_bytes()
|
||||
&& derivation.derivation_path().len() == 5
|
||||
&& derivation.derivation_path()[0]
|
||||
== bip32::ChildNumber::new(32, true).expect("valid")
|
||||
&& derivation.derivation_path()[1]
|
||||
== bip32::ChildNumber::new(params.network_type().coin_type(), true)
|
||||
.expect("valid")
|
||||
&& derivation.derivation_path()[2].is_hardened()
|
||||
&& !derivation.derivation_path()[3].is_hardened()
|
||||
&& !derivation.derivation_path()[4].is_hardened()
|
||||
{
|
||||
let account_index = zip32::AccountId::try_from(
|
||||
derivation.derivation_path()[2].index() - (1 << 31),
|
||||
)
|
||||
.expect("valid");
|
||||
|
||||
let scope = TransparentKeyScope::custom(
|
||||
derivation.derivation_path()[3].index(),
|
||||
)
|
||||
.expect("valid");
|
||||
let address_index = NonHardenedChildIndex::from_index(
|
||||
derivation.derivation_path()[4].index(),
|
||||
)
|
||||
.expect("valid");
|
||||
|
||||
keys.entry(account_index)
|
||||
.or_default()
|
||||
.push(KeyRef::Transparent {
|
||||
index,
|
||||
scope,
|
||||
address_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.expect("no errors")
|
||||
.finish();
|
||||
|
||||
let mut signer =
|
||||
Signer::new(pczt).map_err(|e| anyhow!("Failed to initialize Signer: {:?}", e))?;
|
||||
for (account_index, spends) in keys {
|
||||
let usk = UnifiedSpendingKey::from_seed(¶ms, seed, account_index)?;
|
||||
for keyref in spends {
|
||||
match keyref {
|
||||
KeyRef::Orchard { index } => {
|
||||
signer
|
||||
.sign_orchard(
|
||||
index,
|
||||
&orchard::keys::SpendAuthorizingKey::from(usk.orchard()),
|
||||
)
|
||||
.map_err(|e| {
|
||||
anyhow!("Failed to sign Orchard spend {index}: {:?}", e)
|
||||
})?;
|
||||
}
|
||||
KeyRef::Sapling { index } => {
|
||||
signer
|
||||
.sign_sapling(index, &usk.sapling().expsk.ask)
|
||||
.map_err(|e| {
|
||||
anyhow!("Failed to sign Sapling spend {index}: {:?}", e)
|
||||
})?;
|
||||
}
|
||||
KeyRef::Transparent {
|
||||
index,
|
||||
scope,
|
||||
address_index,
|
||||
} => signer
|
||||
.sign_transparent(
|
||||
index,
|
||||
&usk.transparent()
|
||||
.derive_secret_key(scope, address_index)
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to derive transparent key at .../{:?}/{:?}: {:?}",
|
||||
scope,
|
||||
address_index,
|
||||
e,
|
||||
)
|
||||
})?,
|
||||
)
|
||||
.map_err(|e| {
|
||||
anyhow!("Failed to sign transparent input {index}: {:?}", e)
|
||||
})?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pczt = signer.finish();
|
||||
|
||||
stdout().write_all(&pczt.serialize()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
16
src/main.rs
16
src/main.rs
|
@ -78,6 +78,9 @@ enum Command {
|
|||
|
||||
#[options(help = "send funds to the given address")]
|
||||
Send(commands::send::Command),
|
||||
|
||||
#[options(help = "send funds using PCZTs")]
|
||||
Pczt(commands::pczt::Command),
|
||||
}
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
|
@ -151,7 +154,18 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
Some(Command::ListUnspent(command)) => command.run(opts.wallet_dir),
|
||||
Some(Command::Propose(command)) => command.run(opts.wallet_dir).await,
|
||||
Some(Command::Send(command)) => command.run(opts.wallet_dir).await,
|
||||
_ => Ok(()),
|
||||
Some(Command::Pczt(command)) => match command {
|
||||
commands::pczt::Command::Create(command) => command.run(opts.wallet_dir).await,
|
||||
commands::pczt::Command::Prove(command) => command.run(opts.wallet_dir).await,
|
||||
commands::pczt::Command::Sign(command) => command.run(opts.wallet_dir).await,
|
||||
commands::pczt::Command::Combine(command) => command.run().await,
|
||||
commands::pczt::Command::Send(command) => command.run(opts.wallet_dir).await,
|
||||
#[cfg(feature = "pczt-qr")]
|
||||
commands::pczt::Command::ToQr(command) => command.run(shutdown).await,
|
||||
#[cfg(feature = "pczt-qr")]
|
||||
commands::pczt::Command::FromQr(command) => command.run(shutdown).await,
|
||||
},
|
||||
None => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue