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]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
bip0039 = { version = "0.12", features = ["std", "all-languages"] }
|
bip0039 = { version = "0.12", features = ["std", "all-languages"] }
|
||||||
|
bip32 = "0.5"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
gumdrop = "0.8"
|
gumdrop = "0.8"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
jubjub = "0.10"
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
rayon = "1.7"
|
rayon = "1.7"
|
||||||
rusqlite = { version = "0.32", features = ["time"] }
|
rusqlite = { version = "0.32", features = ["time"] }
|
||||||
|
@ -27,10 +29,11 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
uuid = "1"
|
uuid = "1"
|
||||||
|
|
||||||
orchard = { version = "0.10", default-features = false }
|
orchard = { version = "0.10", default-features = false }
|
||||||
|
pczt = "0.0"
|
||||||
sapling = { package = "sapling-crypto", version = "0.3" }
|
sapling = { package = "sapling-crypto", version = "0.3" }
|
||||||
zcash_address = "0.6"
|
zcash_address = "0.6"
|
||||||
zcash_client_backend = { version = "0.15", features = ["lightwalletd-tonic-tls-webpki-roots", "orchard", "tor"] }
|
zcash_client_backend = { version = "0.15", features = ["lightwalletd-tonic-tls-webpki-roots", "orchard", "pczt", "tor"] }
|
||||||
zcash_client_sqlite = { version = "0.13", features = ["unstable", "orchard"] }
|
zcash_client_sqlite = { version = "0.13", features = ["unstable", "orchard", "serde"] }
|
||||||
zcash_keys = { version = "0.5", features = ["unstable", "orchard"] }
|
zcash_keys = { version = "0.5", features = ["unstable", "orchard"] }
|
||||||
zcash_primitives = "0.20"
|
zcash_primitives = "0.20"
|
||||||
zcash_proofs = "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"] }
|
iso_currency = { version = "0.5", features = ["with-serde"] }
|
||||||
rust_decimal = "1"
|
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
|
# TUI
|
||||||
crossterm = { version = "0.28", optional = true, features = ["event-stream"] }
|
crossterm = { version = "0.28", optional = true, features = ["event-stream"] }
|
||||||
ratatui = { version = "0.28", optional = true }
|
ratatui = { version = "0.28", optional = true }
|
||||||
|
@ -54,6 +63,7 @@ tui-logger = { version = "0.12", optional = true, features = ["tracing-support"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["transparent-inputs"]
|
default = ["transparent-inputs"]
|
||||||
|
pczt-qr = ["dep:nokhwa", "dep:qrcode", "dep:rqrr", "dep:ur"]
|
||||||
transparent-inputs = [
|
transparent-inputs = [
|
||||||
"zcash_client_sqlite/transparent-inputs",
|
"zcash_client_sqlite/transparent-inputs",
|
||||||
]
|
]
|
||||||
|
@ -66,11 +76,14 @@ tui = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
orchard = { git = "https://github.com/zcash/orchard.git", rev = "bcd08e1d23e70c42a338f3e3f79d6f4c0c219805" }
|
||||||
zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||||
zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
sapling-crypto = { git = "https://github.com/zcash/sapling-crypto.git", rev = "29cff9683cdf2f0c522ff3224081dfb4fbc80248" }
|
||||||
zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||||
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||||
zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||||
zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "1e274c892a11cd15f643f08ffa579166d60180bb" }
|
||||||
zip321 = { git = "https://github.com/zcash/librustzcash.git", rev = "e0f04e6c7749751e7f590b2c25275f1fa3421d50" }
|
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_addresses;
|
||||||
pub(crate) mod list_tx;
|
pub(crate) mod list_tx;
|
||||||
pub(crate) mod list_unspent;
|
pub(crate) mod list_unspent;
|
||||||
|
pub(crate) mod pczt;
|
||||||
pub(crate) mod propose;
|
pub(crate) mod propose;
|
||||||
pub(crate) mod reset;
|
pub(crate) mod reset;
|
||||||
pub(crate) mod send;
|
pub(crate) mod send;
|
||||||
|
|
|
@ -28,7 +28,7 @@ impl Command {
|
||||||
println!("Account {}", self.account_id);
|
println!("Account {}", self.account_id);
|
||||||
let (ua, _) = account
|
let (ua, _) = account
|
||||||
.uivk()
|
.uivk()
|
||||||
.default_address(UnifiedAddressRequest::all().unwrap())?;
|
.default_address(UnifiedAddressRequest::all())?;
|
||||||
println!(" Default Address: {}", ua.encode(¶ms));
|
println!(" Default Address: {}", ua.encode(¶ms));
|
||||||
Ok(())
|
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")]
|
#[options(help = "send funds to the given address")]
|
||||||
Send(commands::send::Command),
|
Send(commands::send::Command),
|
||||||
|
|
||||||
|
#[options(help = "send funds using PCZTs")]
|
||||||
|
Pczt(commands::pczt::Command),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), anyhow::Error> {
|
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::ListUnspent(command)) => command.run(opts.wallet_dir),
|
||||||
Some(Command::Propose(command)) => command.run(opts.wallet_dir).await,
|
Some(Command::Propose(command)) => command.run(opts.wallet_dir).await,
|
||||||
Some(Command::Send(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